aiwcli 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  24. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  25. package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
  26. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  27. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  28. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
  29. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  30. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  31. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  32. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  33. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
  34. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  35. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  36. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  37. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  38. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  39. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  40. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  41. package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
  42. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  43. package/dist/templates/cc-native/.claude/settings.json +175 -185
  44. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  45. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  46. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  47. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  70. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
  71. package/oclif.manifest.json +1 -1
  72. package/package.json +1 -1
  73. package/dist/templates/_shared/hooks/__init__.py +0 -16
  74. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  75. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  76. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  87. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  88. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  89. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  90. package/dist/templates/_shared/hooks/session_end.py +0 -173
  91. package/dist/templates/_shared/hooks/session_start.py +0 -206
  92. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  93. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  94. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  95. package/dist/templates/_shared/lib/__init__.py +0 -1
  96. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  98. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  100. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  108. package/dist/templates/_shared/lib/base/constants.py +0 -358
  109. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  110. package/dist/templates/_shared/lib/base/inference.py +0 -307
  111. package/dist/templates/_shared/lib/base/logger.py +0 -305
  112. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  113. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  114. package/dist/templates/_shared/lib/base/utils.py +0 -263
  115. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  116. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  118. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  130. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  131. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  132. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  133. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  134. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  135. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  137. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  138. package/dist/templates/_shared/lib/templates/README.md +0 -206
  139. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  140. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  142. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  145. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  146. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  149. package/dist/templates/_shared/scripts/status_line.py +0 -716
  150. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  151. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  152. package/dist/templates/cc-native/MIGRATION.md +0 -86
  153. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  154. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  160. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  161. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  162. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  163. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  164. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  165. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  173. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  174. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  175. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  176. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  185. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  186. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  187. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  189. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -1,954 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- CC-Native Plan Review Hook (Unified)
4
-
5
- Claude Code PreToolUse hook that intercepts ExitPlanMode and
6
- automatically reviews plans using:
7
- 1. CLI reviewers (Codex + Gemini)
8
- 2. Plan orchestrator for complexity analysis
9
- 3. Claude Code agents in parallel
10
-
11
- Trigger: ExitPlanMode tool use (PreToolUse - runs BEFORE user approval prompt)
12
-
13
- Features:
14
- - Detects plans via ExitPlanMode PreToolUse
15
- - Phase 1: Runs CLI reviewers (Codex/Gemini) if enabled
16
- - Phase 2: Runs orchestrator to analyze complexity and select agents
17
- - Phase 3: Runs selected agents in parallel
18
- - Phase 4: Generates combined output (single JSON + single Markdown)
19
- - Returns feedback to Claude via hook additionalContext
20
- - Optional blocking on FAIL verdict
21
-
22
- Configuration: _cc-native/plan-review.config.json -> planReview, agentReview
23
-
24
- Output: _output/cc-native/plans/{YYYY-MM-DD}/{slug}/reviews/
25
- - review.json (combined review data)
26
- - review.md (combined markdown)
27
- - {reviewer}.json (individual reviewer results)
28
- """
29
-
30
- import json
31
- import os
32
- import random
33
- import sys
34
- from concurrent.futures import ThreadPoolExecutor, as_completed
35
- from datetime import datetime
36
- from pathlib import Path
37
- from typing import Any, Dict, List, Optional
38
-
39
- # Import shared library
40
- try:
41
- _lib = Path(__file__).parent.parent / "lib"
42
- sys.path.insert(0, str(_lib))
43
-
44
- # Add shared library path
45
- _shared = Path(__file__).parent.parent.parent / "_shared"
46
- sys.path.insert(0, str(_shared))
47
-
48
- # Import subprocess and hook utilities
49
- from lib.base.subprocess_utils import is_internal_call
50
- from lib.base.hook_utils import emit_context, emit_context_and_block
51
- from lib.base.logger import log_debug, log_info, log_warn, log_error, log_diagnostic
52
-
53
- from utils import (
54
- DEFAULT_DISPLAY,
55
- DEFAULT_SANITIZATION,
56
- REVIEW_SCHEMA,
57
- ReviewerResult,
58
- CombinedReviewResult,
59
- project_dir,
60
- eprint,
61
- find_plan_file,
62
- compute_plan_hash,
63
- compute_review_decision,
64
- is_plan_already_reviewed,
65
- was_plan_previously_denied,
66
- mark_plan_reviewed,
67
- worst_verdict,
68
- format_combined_markdown,
69
- write_combined_artifacts,
70
- build_inline_review_summary,
71
- extract_top_issues_text,
72
- build_high_issues_document,
73
- load_config,
74
- get_display_settings,
75
- )
76
- from reviewers import (
77
- run_codex_review,
78
- run_gemini_review,
79
- run_agent_review,
80
- AgentConfig,
81
- OrchestratorConfig,
82
- )
83
- from orchestrator import (
84
- run_orchestrator,
85
- DEFAULT_AGENT_SELECTION,
86
- DEFAULT_COMPLEXITY_CATEGORIES,
87
- )
88
- # Import shared context system
89
- from lib.context.context_store import (
90
- get_context_by_session_id,
91
- get_all_contexts,
92
- )
93
- from lib.base.constants import get_context_reviews_dir, get_review_folder_path, get_context_dir
94
- from debug import debug_log, debug_raw
95
- except ImportError as e:
96
- try:
97
- from lib.base.logger import log_error as _early_log_error
98
- _early_log_error("cc-native-plan-review", f"Failed to import lib: {e}")
99
- except Exception:
100
- print(f"[cc-native-plan-review] Failed to import lib: {e}", file=sys.stderr)
101
- print(json.dumps({
102
- "hookSpecificOutput": {
103
- "additionalContext": f"[Plan Review Error] Failed to import required module: {e}. The plan review hook could not load its dependencies.",
104
- }
105
- }, ensure_ascii=True))
106
- sys.exit(0) # Non-blocking failure
107
-
108
- # Add scripts directory to path for aggregate_agents import
109
- _scripts_dir = Path(__file__).parent.parent / "scripts"
110
- if str(_scripts_dir) not in sys.path:
111
- sys.path.insert(0, str(_scripts_dir))
112
-
113
- try:
114
- from aggregate_agents import aggregate_agents
115
- except ImportError:
116
- def aggregate_agents(agents_dir: Path) -> List[Dict[str, Any]]:
117
- log_warn("cc-native-plan-review", "aggregate_agents not found")
118
- return []
119
-
120
-
121
- def skip_with_info(reason: str) -> int:
122
- """Exit hook with informational additionalContext instead of silently.
123
-
124
- This ensures Claude always sees WHY the plan review was skipped,
125
- making failures diagnosable instead of invisible.
126
- """
127
- log_info("cc-native-plan-review", f"Skipping: {reason}")
128
- emit_context(f"[Plan Review Skipped] {reason}", ensure_ascii=True)
129
- return 0
130
-
131
-
132
- # ---------------------------
133
- # Default Configuration
134
- # ---------------------------
135
-
136
- DEFAULT_AGENTS: List[Dict[str, Any]] = [
137
- # Mandatory agents
138
- {"name": "handoff-readiness", "model": "sonnet", "focus": "fresh context execution readiness", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
139
- {"name": "clarity-auditor", "model": "sonnet", "focus": "communication clarity and execution readiness", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
140
- {"name": "skeptic", "model": "sonnet", "focus": "problem-solution alignment and assumption validation", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
141
- {"name": "documentation-philosophy", "model": "sonnet", "focus": "knowledge capture and documentation placement", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
142
- # Risk family
143
- {"name": "risk-premortem", "model": "sonnet", "focus": "pre-mortem failure analysis", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
144
- {"name": "risk-fmea", "model": "sonnet", "focus": "systematic failure mode analysis", "enabled": True, "categories": ["code", "infrastructure", "design"]},
145
- {"name": "risk-dependency", "model": "sonnet", "focus": "dependency chain and blast radius analysis", "enabled": True, "categories": ["code", "infrastructure"]},
146
- {"name": "risk-reversibility", "model": "sonnet", "focus": "decision reversibility and optionality", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
147
- # Completeness family
148
- {"name": "completeness-gaps", "model": "sonnet", "focus": "structural gap analysis", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
149
- {"name": "completeness-feasibility", "model": "sonnet", "focus": "feasibility and resource analysis", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
150
- {"name": "completeness-ordering", "model": "sonnet", "focus": "step ordering and critical path analysis", "enabled": True, "categories": ["code", "infrastructure", "design"]},
151
- # Architecture family
152
- {"name": "arch-structure", "model": "sonnet", "focus": "coupling, cohesion, and boundary analysis", "enabled": True, "categories": ["code", "infrastructure", "design"]},
153
- {"name": "arch-evolution", "model": "sonnet", "focus": "evolutionary architecture and change amplification", "enabled": True, "categories": ["code", "infrastructure", "design"]},
154
- {"name": "arch-patterns", "model": "sonnet", "focus": "pattern selection and technology fit", "enabled": True, "categories": ["code", "infrastructure"]},
155
- # Verification family
156
- {"name": "verify-coverage", "model": "sonnet", "focus": "verification coverage mapping", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
157
- {"name": "verify-strength", "model": "sonnet", "focus": "test quality and mutation analysis", "enabled": True, "categories": ["code", "infrastructure"]},
158
- # Trade-off family
159
- {"name": "tradeoff-costs", "model": "sonnet", "focus": "opportunity cost and capability sacrifice", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
160
- {"name": "tradeoff-stakeholders", "model": "sonnet", "focus": "stakeholder impact and cost-benefit asymmetry", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
161
- # Standalone agents
162
- {"name": "scope-boundary", "model": "sonnet", "focus": "scope drift and boundary enforcement", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
163
- {"name": "hidden-complexity", "model": "sonnet", "focus": "understated complexity and hidden difficulty", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
164
- {"name": "simplicity-guardian", "model": "sonnet", "focus": "over-engineering and unnecessary complexity", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
165
- {"name": "devils-advocate", "model": "sonnet", "focus": "contrarian analysis and reductio ad absurdum", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
166
- {"name": "assumption-tracer", "model": "sonnet", "focus": "dependency chains and foundational assumptions", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
167
- {"name": "incremental-delivery", "model": "sonnet", "focus": "incremental delivery and vertical slicing", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
168
- {"name": "constraint-validator", "model": "sonnet", "focus": "constraint identification and satisfaction", "enabled": True, "categories": ["code", "infrastructure", "documentation", "design", "research", "life", "business"]},
169
- ]
170
-
171
- DEFAULT_ORCHESTRATOR: Dict[str, Any] = {
172
- "enabled": True,
173
- "model": "opus",
174
- "timeout": 60,
175
- }
176
-
177
- DEFAULT_AGENT_MODEL: str = "sonnet"
178
-
179
- DEFAULT_REVIEW_ITERATIONS: Dict[str, int] = {
180
- "simple": 1,
181
- "medium": 2,
182
- "high": 2,
183
- }
184
-
185
-
186
- def resolve_mandatory_agents(config_value, complexity: str) -> set:
187
- """Resolve mandatory agent names based on config format and complexity.
188
-
189
- Supports two formats:
190
- - Legacy (list): ["a", "b"] — all treated as 'always'
191
- - Structured (dict): {"always": [...], "medium+": [...], "high": [...]}
192
- """
193
- if isinstance(config_value, list):
194
- return set(config_value)
195
-
196
- if not isinstance(config_value, dict):
197
- return {"handoff-readiness", "clarity-auditor", "skeptic"}
198
-
199
- names = set(config_value.get("always", []))
200
-
201
- if complexity in ("medium", "high"):
202
- names.update(config_value.get("medium+", []))
203
-
204
- if complexity == "high":
205
- names.update(config_value.get("high", []))
206
-
207
- return names
208
-
209
-
210
- # ---------------------------
211
- # Context-based State Management
212
- # ---------------------------
213
-
214
- def get_active_context_for_review(session_id: str, project_root: Path) -> Optional[Any]:
215
- """Find active context for plan review.
216
-
217
- Strategy:
218
- 1. Find context by session_id
219
- 2. Fallback: Single context in 'planning' mode
220
- 3. Return None if multiple planning contexts or no planning contexts found
221
-
222
- Only triggers for contexts in 'planning' mode, not 'handoff_pending' or other modes.
223
-
224
- Args:
225
- session_id: Current session ID
226
- project_root: Project root path
227
-
228
- Returns:
229
- Context object or None
230
- """
231
- # Strategy 1: Find by session_id
232
- context = get_context_by_session_id(session_id, project_root)
233
- if context:
234
- log_info("cc-native-plan-review", f"Found context by session_id: {context.id}")
235
- return context
236
-
237
- # Strategy 2: Single planning context (only planning mode)
238
- all_active = get_all_contexts(status="active", project_root=project_root)
239
- # In the new system, "planning" is runtime-only (not persisted).
240
- # Since this hook fires during ExitPlanMode, any active non-idle context is a candidate.
241
- planning_contexts = [c for c in all_active if c.mode in ("active", "has_plan")]
242
- if len(planning_contexts) == 1:
243
- log_info("cc-native-plan-review", f"Found single planning context: {planning_contexts[0].id}")
244
- return planning_contexts[0]
245
-
246
- # Multiple or no planning contexts found
247
- if len(planning_contexts) > 1:
248
- log_warn("cc-native-plan-review", f"Multiple planning contexts ({len(planning_contexts)}), cannot determine which to use")
249
- elif len(all_active) > 0:
250
- modes = [c.mode for c in all_active]
251
- log_info("cc-native-plan-review", f"Found {len(all_active)} active context(s) with modes {modes}, but none in 'planning' mode")
252
- else:
253
- log_info("cc-native-plan-review", "No active contexts found")
254
- return None
255
-
256
-
257
- def load_iteration_state(reviews_dir: Path) -> Optional[Dict[str, Any]]:
258
- """Load iteration state from context reviews folder.
259
-
260
- Args:
261
- reviews_dir: Path to the reviews directory
262
-
263
- Returns:
264
- Iteration state dict or None if not found
265
- """
266
- iteration_file = reviews_dir / "iteration.json"
267
- if not iteration_file.exists():
268
- return None
269
-
270
- try:
271
- return json.loads(iteration_file.read_text(encoding="utf-8"))
272
- except Exception as e:
273
- log_error("cc-native-plan-review", f"Failed to load iteration state: {e}")
274
- return None
275
-
276
-
277
- def save_iteration_state(reviews_dir: Path, state: Dict[str, Any]) -> bool:
278
- """Save iteration state to context reviews folder.
279
-
280
- Args:
281
- reviews_dir: Path to the reviews directory
282
- state: Iteration state dict
283
-
284
- Returns:
285
- True on success, False on failure
286
- """
287
- iteration_file = reviews_dir / "iteration.json"
288
- try:
289
- reviews_dir.mkdir(parents=True, exist_ok=True)
290
- state["schema_version"] = "1.0.0"
291
- iteration_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
292
- return True
293
- except Exception as e:
294
- log_error("cc-native-plan-review", f"Failed to save iteration state: {e}")
295
- return False
296
-
297
-
298
- def get_iteration_state_from_context(
299
- reviews_dir: Path,
300
- complexity: str,
301
- config: Optional[Dict[str, Any]] = None,
302
- ) -> Dict[str, Any]:
303
- """Get or initialize iteration state based on complexity.
304
-
305
- Args:
306
- reviews_dir: Path to the reviews directory
307
- complexity: Plan complexity level (simple/medium/high)
308
- config: Optional config dict with reviewIterations settings
309
-
310
- Returns:
311
- Iteration dict with: current, max, complexity, history
312
- """
313
- existing = load_iteration_state(reviews_dir)
314
- if existing:
315
- return existing
316
-
317
- # Initialize new iteration state
318
- review_iterations = DEFAULT_REVIEW_ITERATIONS.copy()
319
- if config:
320
- review_iterations.update(config.get("reviewIterations", {}))
321
- max_iterations = review_iterations.get(complexity, 1)
322
-
323
- return {
324
- "current": 1,
325
- "max": max_iterations,
326
- "complexity": complexity,
327
- "history": [],
328
- }
329
-
330
-
331
- def update_iteration_state_in_context(
332
- reviews_dir: Path,
333
- iteration: Dict[str, Any],
334
- plan_hash: str,
335
- verdict: str,
336
- ) -> Dict[str, Any]:
337
- """Record review result in iteration history.
338
-
339
- Args:
340
- reviews_dir: Path to the reviews directory
341
- iteration: The iteration state dict
342
- plan_hash: Hash of the current plan content
343
- verdict: Review verdict (pass/warn/fail)
344
-
345
- Returns:
346
- Updated iteration state dict
347
- """
348
- from datetime import datetime
349
-
350
- iteration["history"].append({
351
- "hash": plan_hash,
352
- "verdict": verdict,
353
- "timestamp": datetime.now().isoformat(),
354
- })
355
- return iteration
356
-
357
-
358
- def should_continue_iterating_context(
359
- iteration: Dict[str, Any],
360
- review_score: float,
361
- config: Optional[Dict[str, Any]] = None,
362
- ) -> bool:
363
- """Determine if more review iterations are needed.
364
-
365
- Args:
366
- iteration: The iteration state dict
367
- review_score: Score from compute_review_decision (0.0 = all pass, >0 = concerns)
368
- config: Optional config dict with earlyExitOnAllPass setting
369
-
370
- Returns:
371
- True if more iterations needed, False otherwise
372
- """
373
- current = iteration.get("current", 1)
374
- max_iter = iteration.get("max", 1)
375
-
376
- # At or past max iterations - no more iterations
377
- if current >= max_iter:
378
- log_info("cc-native-plan-review", f"At max iterations ({current}/{max_iter}), no more iterations")
379
- return False
380
-
381
- # Check early exit on all pass
382
- early_exit = False
383
- if config:
384
- early_exit = config.get("earlyExitOnAllPass", False)
385
- if early_exit and review_score == 0.0:
386
- log_info("cc-native-plan-review", "All reviewers passed (score=0.0) and earlyExitOnAllPass=true, exiting early")
387
- return False
388
-
389
- # More iterations available and score is not zero (or early exit disabled)
390
- log_info("cc-native-plan-review", f"Continuing to next iteration ({current + 1}/{max_iter}), score={review_score:.2f}")
391
- return True
392
-
393
-
394
- # ---------------------------
395
- # Settings Loading
396
- # ---------------------------
397
-
398
- def load_settings(proj_dir: Path) -> Dict[str, Any]:
399
- """Load CC-Native settings from _cc-native/plan-review.config.json"""
400
- defaults = {
401
- "planReview": {
402
- "enabled": True,
403
- "reviewers": {
404
- "codex": {"enabled": True, "model": "", "timeout": 120},
405
- "gemini": {"enabled": False, "model": "", "timeout": 120},
406
- },
407
- "display": DEFAULT_DISPLAY.copy(),
408
- },
409
- "agentReview": {
410
- "enabled": True,
411
- "orchestrator": DEFAULT_ORCHESTRATOR.copy(),
412
- "timeout": 180,
413
- "warnThreshold": 0.5,
414
- "highIssueThreshold": 3,
415
- "legacyMode": False,
416
- "display": DEFAULT_DISPLAY.copy(),
417
- "agentSelection": DEFAULT_AGENT_SELECTION.copy(),
418
- "agentDefaults": {"model": DEFAULT_AGENT_MODEL},
419
- "complexityCategories": DEFAULT_COMPLEXITY_CATEGORIES.copy(),
420
- "sanitization": DEFAULT_SANITIZATION.copy(),
421
- },
422
- }
423
-
424
- config = load_config(proj_dir)
425
- if not config:
426
- return defaults
427
-
428
- # Merge planReview settings
429
- plan_review = config.get("planReview", {})
430
- merged_plan = defaults["planReview"].copy()
431
- merged_plan.update(plan_review)
432
- if "reviewers" in plan_review:
433
- merged_plan["reviewers"] = defaults["planReview"]["reviewers"].copy()
434
- merged_plan["reviewers"].update(plan_review["reviewers"])
435
- merged_plan["display"] = get_display_settings(config, "planReview")
436
-
437
- # Merge agentReview settings
438
- agent_review = config.get("agentReview", {})
439
- merged_agent = defaults["agentReview"].copy()
440
- merged_agent.update(agent_review)
441
-
442
- # Handle orchestrator nested config
443
- if "orchestrator" not in merged_agent or not isinstance(merged_agent["orchestrator"], dict):
444
- merged_agent["orchestrator"] = DEFAULT_ORCHESTRATOR.copy()
445
- else:
446
- orch = DEFAULT_ORCHESTRATOR.copy()
447
- orch.update(merged_agent["orchestrator"])
448
- merged_agent["orchestrator"] = orch
449
-
450
- merged_agent["display"] = get_display_settings(config, "agentReview")
451
- merged_agent["agentSelection"] = {**DEFAULT_AGENT_SELECTION, **config.get("agentSelection", {})}
452
- merged_agent["agentDefaults"] = {**{"model": DEFAULT_AGENT_MODEL}, **config.get("agentDefaults", {})}
453
- merged_agent["complexityCategories"] = config.get("complexityCategories", DEFAULT_COMPLEXITY_CATEGORIES.copy())
454
- merged_agent["sanitization"] = {**DEFAULT_SANITIZATION, **config.get("sanitization", {})}
455
-
456
- # Merge reviewIterations settings
457
- merged_agent["reviewIterations"] = {**DEFAULT_REVIEW_ITERATIONS, **agent_review.get("reviewIterations", {})}
458
- merged_agent["earlyExitOnAllPass"] = agent_review.get("earlyExitOnAllPass", False)
459
-
460
- return {"planReview": merged_plan, "agentReview": merged_agent}
461
-
462
-
463
- def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None) -> List[AgentConfig]:
464
- """Load agent library by auto-detecting from frontmatter.
465
-
466
- Agents are loaded from _cc-native/agents/ directory. The markdown body
467
- of each agent file becomes the system_prompt for --system-prompt invocation.
468
- """
469
- # aggregate_agents now defaults to _cc-native/agents/ relative to the script
470
- agents_data = aggregate_agents()
471
-
472
- default_model = DEFAULT_AGENT_MODEL
473
- if settings:
474
- default_model = settings.get("agentDefaults", {}).get("model", DEFAULT_AGENT_MODEL)
475
-
476
- if not agents_data:
477
- log_info("cc-native-plan-review", "No agents found in frontmatter, using defaults")
478
- return [
479
- AgentConfig(
480
- name=a["name"],
481
- model=a.get("model", default_model),
482
- focus=a.get("focus", "general review"),
483
- enabled=a.get("enabled", True),
484
- categories=a.get("categories", ["code"]),
485
- )
486
- for a in DEFAULT_AGENTS
487
- ]
488
-
489
- agents = []
490
- for a in agents_data:
491
- if a.get("name") == "plan-orchestrator":
492
- continue
493
- agents.append(AgentConfig(
494
- name=a["name"],
495
- model=a.get("model", default_model),
496
- focus=a.get("focus", "general review"),
497
- enabled=a.get("enabled", True),
498
- categories=a.get("categories", ["code"]),
499
- description=a.get("description", ""),
500
- system_prompt=a.get("system_prompt", ""),
501
- ))
502
-
503
- return agents
504
-
505
-
506
- # ---------------------------
507
- # Main Hook
508
- # ---------------------------
509
-
510
- def main() -> int:
511
- log_info("cc-native-plan-review", "Unified hook started (PreToolUse)")
512
-
513
- # Skip if internal subprocess call (orchestrator, agents)
514
- if is_internal_call():
515
- log_debug("cc-native-plan-review", "Skipping: internal subprocess call")
516
- return 0
517
-
518
- try:
519
- payload = json.load(sys.stdin)
520
- except json.JSONDecodeError as e:
521
- return skip_with_info(f"Invalid JSON input from Claude Code: {e}")
522
-
523
- tool_name = payload.get("tool_name")
524
- log_debug("cc-native-plan-review", f"tool_name: {tool_name}")
525
-
526
- # Only process ExitPlanMode
527
- if tool_name != "ExitPlanMode":
528
- log_debug("cc-native-plan-review", "Skipping: not ExitPlanMode")
529
- return 0
530
-
531
- session_id = str(payload.get("session_id", "unknown"))
532
- base = project_dir(payload)
533
- settings = load_settings(base)
534
-
535
- plan_settings = settings.get("planReview", {})
536
- agent_settings = settings.get("agentReview", {})
537
-
538
- plan_review_enabled = plan_settings.get("enabled", True)
539
- agent_review_enabled = agent_settings.get("enabled", True)
540
-
541
- if not plan_review_enabled and not agent_review_enabled:
542
- log_info("cc-native-plan-review", "Skipping: both plan and agent review disabled")
543
- return 0
544
-
545
- # Find and read plan FIRST (state file is keyed by plan path)
546
- plan_path = find_plan_file()
547
- if not plan_path:
548
- return skip_with_info("No plan file found in ~/.claude/plans/. The plan may not have been written yet.")
549
-
550
- try:
551
- plan = Path(plan_path).read_text(encoding="utf-8").strip()
552
- except Exception as e:
553
- return skip_with_info(f"Failed to read plan file: {e}")
554
-
555
- if not plan:
556
- return skip_with_info("Plan file exists but is empty.")
557
-
558
- log_info("cc-native-plan-review", f"Found plan at: {plan_path}")
559
- log_debug("cc-native-plan-review", f"Plan length: {len(plan)} chars")
560
- log_diagnostic("cc-native-plan-review", "receive", f"plan_size={len(plan)}, session={session_id[:8]}",
561
- inputs={"plan_hash": compute_plan_hash(plan), "plan_size": len(plan),
562
- "session_id": session_id[:12]})
563
-
564
- # Find active context for this review (required)
565
- active_context = get_active_context_for_review(session_id, base)
566
-
567
- if not active_context:
568
- return skip_with_info("No active planning context found for this session. The context system may not have a context in 'planning' mode.")
569
-
570
- # Get base reviews dir from shared lib, then add cc-native namespace
571
- reviews_dir = get_context_reviews_dir(active_context.id, base) / "cc-native"
572
- log_debug("cc-native-plan-review", f"Using context reviews dir: {reviews_dir}")
573
-
574
- # Get context path for debug logging
575
- context_path = get_context_dir(active_context.id, base)
576
- log_debug("cc-native-plan-review", f"Context path for debug: {context_path}")
577
-
578
- # Plan-hash deduplication (decision-aware)
579
- plan_hash = compute_plan_hash(plan)
580
- log_debug("cc-native-plan-review", f"Plan hash: {plan_hash}")
581
- if is_plan_already_reviewed(session_id, plan_hash):
582
- if was_plan_previously_denied(session_id, plan_hash):
583
- # Plan was denied and hasn't changed — block, don't re-review
584
- emit_context_and_block(
585
- "[Plan Review] Plan content unchanged since last review which found issues.",
586
- "Plan unchanged since denial. Modify the plan to address review findings, "
587
- "then attempt ExitPlanMode again.",
588
- )
589
- return 0
590
- else:
591
- # Plan was reviewed and allowed — skip review, allow through
592
- return skip_with_info("Plan already reviewed and approved (same hash).")
593
-
594
- # Initialize combined result
595
- cli_results: Dict[str, ReviewerResult] = {}
596
- orch_result = None
597
- agent_results: Dict[str, ReviewerResult] = {}
598
- all_verdicts: List[str] = []
599
- iteration_state: Optional[Dict[str, Any]] = None
600
- detected_complexity: str = "medium" # Will be updated by orchestrator
601
-
602
- # ============================================
603
- # PHASE 1 & 2: CLI Reviewers + Orchestrator (PARALLEL)
604
- # ============================================
605
- # Run CLI reviewers and orchestrator concurrently for speed
606
- reviewers_config = plan_settings.get("reviewers", {}) if plan_review_enabled else {}
607
- codex_enabled = plan_review_enabled and reviewers_config.get("codex", {}).get("enabled", True)
608
- gemini_enabled = plan_review_enabled and reviewers_config.get("gemini", {}).get("enabled", False)
609
-
610
- agent_library = load_agent_library(base, agent_settings) if agent_review_enabled else []
611
- # Load all agents regardless of enabled status - enabled:false only prevents
612
- # Claude Code auto-suggestion, not plan-review usage
613
- enabled_agents = agent_library
614
- timeout = agent_settings.get("timeout", 120)
615
- legacy_mode = agent_settings.get("legacyMode", False)
616
-
617
- orch_settings = agent_settings.get("orchestrator", DEFAULT_ORCHESTRATOR)
618
- orchestrator_config = OrchestratorConfig(
619
- enabled=orch_settings.get("enabled", True) and agent_review_enabled,
620
- model=orch_settings.get("model", "haiku"),
621
- timeout=orch_settings.get("timeout", 30),
622
- )
623
-
624
- # Two-phase mandatory resolution:
625
- # Phase 1 (pre-orchestrator): Only "always" mandatory agents excluded from orchestrator pool
626
- # Phase 2 (post-orchestrator): Full mandatory set including conditional agents
627
- mandatory_config = agent_settings.get("mandatoryAgents", [
628
- "handoff-readiness", "clarity-auditor", "skeptic"
629
- ])
630
- always_mandatory = resolve_mandatory_agents(mandatory_config, "simple")
631
- mandatory_names = always_mandatory
632
-
633
- log_debug("cc-native-plan-review", f"Codex enabled: {codex_enabled}, Gemini enabled: {gemini_enabled}")
634
- log_debug("cc-native-plan-review", f"Agent library: {[a.name for a in agent_library]}")
635
- log_debug("cc-native-plan-review", f"Enabled agents: {[a.name for a in enabled_agents]}")
636
- log_debug("cc-native-plan-review", f"Mandatory agents: {sorted(mandatory_names)}")
637
- log_debug("cc-native-plan-review", f"Orchestrator enabled: {orchestrator_config.enabled}")
638
-
639
- # Run CLI reviewers + orchestrator in parallel
640
- phase1_tasks = []
641
- if codex_enabled:
642
- phase1_tasks.append(("codex", lambda: run_codex_review(plan, REVIEW_SCHEMA, plan_settings)))
643
- if gemini_enabled:
644
- phase1_tasks.append(("gemini", lambda: run_gemini_review(plan, REVIEW_SCHEMA, plan_settings)))
645
- if orchestrator_config.enabled and enabled_agents and not legacy_mode:
646
- phase1_tasks.append(("orchestrator", lambda: run_orchestrator(plan, enabled_agents, orchestrator_config, agent_settings, mandatory_names=always_mandatory)))
647
-
648
- log_info("cc-native-plan-review", f"=== PHASE 1: Running {len(phase1_tasks)} tasks in parallel ===")
649
-
650
- phase1_results: Dict[str, Any] = {}
651
- if phase1_tasks:
652
- with ThreadPoolExecutor(max_workers=len(phase1_tasks)) as executor:
653
- futures = {executor.submit(task_fn): name for name, task_fn in phase1_tasks}
654
- for future in as_completed(futures):
655
- name = futures[future]
656
- try:
657
- phase1_results[name] = future.result()
658
- log_info("cc-native-plan-review", f"{name} completed")
659
- except Exception as ex:
660
- log_error("cc-native-plan-review", f"{name} failed: {ex}")
661
- phase1_results[name] = None
662
-
663
- # Collect CLI results
664
- if "codex" in phase1_results and phase1_results["codex"]:
665
- cli_results["codex"] = phase1_results["codex"]
666
- if "gemini" in phase1_results and phase1_results["gemini"]:
667
- cli_results["gemini"] = phase1_results["gemini"]
668
-
669
- # Get orchestrator result
670
- if "orchestrator" in phase1_results and phase1_results["orchestrator"]:
671
- orch_result = phase1_results["orchestrator"]
672
-
673
- # ============================================
674
- # PHASE 2: Agent Selection (from orchestrator result)
675
- # ============================================
676
- if agent_review_enabled:
677
- log_info("cc-native-plan-review", "=== PHASE 2: Agent Selection ===")
678
-
679
- selected_agents: List[AgentConfig] = []
680
-
681
- # Load fallback config (mandatory_names already computed above)
682
- fallback_by_complexity = agent_settings.get("fallbackByComplexity", {
683
- "simple": 0, "medium": 5, "high": 9
684
- })
685
-
686
- if enabled_agents:
687
- # Split into mandatory and non-mandatory pools
688
- mandatory_agents = [a for a in enabled_agents if a.name in mandatory_names]
689
- non_mandatory = [a for a in enabled_agents if a.name not in mandatory_names]
690
-
691
- log_debug("cc-native-plan-review", f"Mandatory agents: {[a.name for a in mandatory_agents]}")
692
- log_debug("cc-native-plan-review", f"Non-mandatory pool: {len(non_mandatory)} agents")
693
-
694
- if orch_result and not legacy_mode:
695
- detected_complexity = orch_result.complexity
696
-
697
- # Phase 2: Recompute mandatory set with actual complexity
698
- mandatory_names = resolve_mandatory_agents(mandatory_config, detected_complexity)
699
- mandatory_agents = [a for a in enabled_agents if a.name in mandatory_names]
700
- non_mandatory = [a for a in enabled_agents if a.name not in mandatory_names]
701
-
702
- # Get orchestrator's additional selections (excluding mandatory since they always run)
703
- orch_selected_names = set(orch_result.selected_agents) - mandatory_names
704
- orch_selected = [a for a in non_mandatory if a.name in orch_selected_names]
705
-
706
- log_debug("cc-native-plan-review", f"Orchestrator selected (non-mandatory): {[a.name for a in orch_selected]}")
707
-
708
- # Diagnostic: warn if orchestrator returned names not in our agent pool
709
- unmatched = orch_selected_names - {a.name for a in non_mandatory}
710
- if unmatched:
711
- log_warn("cc-native-plan-review", f"Orchestrator selected unknown agents: {unmatched}")
712
-
713
- # Enforce minimum agent count — top up with random agents if orchestrator selected too few
714
- min_additional = fallback_by_complexity.get(detected_complexity, 5)
715
- if len(orch_selected) < min_additional and non_mandatory:
716
- remaining = [a for a in non_mandatory if a not in orch_selected]
717
- top_up_count = min(min_additional - len(orch_selected), len(remaining))
718
- if top_up_count > 0:
719
- top_up = random.sample(remaining, top_up_count)
720
- orch_selected.extend(top_up)
721
- log_debug("cc-native-plan-review", f"Topped up {top_up_count} agents to meet {detected_complexity} minimum: {[a.name for a in top_up]}")
722
-
723
- # Combine: mandatory + orchestrator/fallback selection
724
- selected_agents = mandatory_agents + orch_selected
725
- log_info("cc-native-plan-review", f"Final selection: {len(selected_agents)} agents ({len(mandatory_agents)} mandatory + {len(orch_selected)} additional)")
726
- else:
727
- log_info("cc-native-plan-review", "Running in legacy mode (all enabled agents)")
728
- detected_complexity = "medium" # Default for legacy mode
729
- mandatory_names = resolve_mandatory_agents(mandatory_config, detected_complexity)
730
- selected_agents = enabled_agents
731
-
732
- log_diagnostic("cc-native-plan-review", "decide",
733
- f"Selected {len(selected_agents)} agents, complexity={detected_complexity}",
734
- decision="agents_selected",
735
- reasoning=f"orchestrator={orch_result is not None}, legacy={legacy_mode}",
736
- inputs={"agents": [a.name for a in selected_agents],
737
- "complexity": detected_complexity,
738
- "mandatory_count": len([a for a in selected_agents if a.name in mandatory_names])})
739
-
740
- # Initialize iteration state based on complexity (after orchestrator runs)
741
- if reviews_dir:
742
- iteration_state = get_iteration_state_from_context(reviews_dir, detected_complexity, agent_settings)
743
- log_debug("cc-native-plan-review", f"Iteration state: {iteration_state['current']}/{iteration_state['max']} ({detected_complexity})")
744
-
745
- # PHASE 3: Run selected agents in parallel
746
- if selected_agents:
747
- log_info("cc-native-plan-review", "=== PHASE 3: Agent Reviews ===")
748
- max_parallel = agent_settings.get("maxParallelAgents", 0) # 0 = unlimited
749
- num_workers = len(selected_agents) if max_parallel <= 0 else min(max_parallel, len(selected_agents))
750
- log_info("cc-native-plan-review", f"Launching {len(selected_agents)} agents in parallel (workers={num_workers})")
751
-
752
- # Debug log the agent review start
753
- debug_log(context_path, session_id, "hook", "agent_review_start", {
754
- "agents": [a.name for a in selected_agents],
755
- "timeout": timeout,
756
- "complexity": detected_complexity,
757
- })
758
-
759
- with ThreadPoolExecutor(max_workers=num_workers) as executor:
760
- futures = {
761
- executor.submit(run_agent_review, plan, agent, REVIEW_SCHEMA, timeout, context_path, session_id): agent
762
- for agent in selected_agents
763
- }
764
- for future in as_completed(futures):
765
- agent = futures[future]
766
- try:
767
- result = future.result()
768
- agent_results[agent.name] = result
769
- log_info("cc-native-plan-review", f"{agent.name} completed with verdict: {result.verdict}")
770
- except Exception as ex:
771
- log_error("cc-native-plan-review", f"{agent.name} failed with exception: {ex}")
772
- agent_results[agent.name] = ReviewerResult(
773
- name=agent.name,
774
- ok=False,
775
- verdict="error",
776
- data={},
777
- raw="",
778
- err=str(ex),
779
- )
780
-
781
- # ============================================
782
- # Per-agent high-severity threshold: override verdict to "fail" if threshold met
783
- # ============================================
784
- high_issue_threshold = agent_settings.get("highIssueThreshold", 3)
785
- all_verdicts = [] # Recompute with overrides applied
786
-
787
- for r in list(cli_results.values()) + list(agent_results.values()):
788
- if not r.verdict or r.verdict in ("skip", "error"):
789
- continue
790
- agent_high = sum(
791
- 1 for issue in (r.data.get("issues", []) if r.data else [])
792
- if issue.get("severity") == "high"
793
- )
794
- if agent_high >= high_issue_threshold:
795
- log_info("cc-native-plan-review",
796
- f"{r.name}: verdict overridden to 'fail' ({agent_high} high issues >= {high_issue_threshold})")
797
- r.verdict = "fail"
798
- all_verdicts.append(r.verdict)
799
-
800
- # ============================================
801
- # PHASE 4: Generate Combined Output
802
- # ============================================
803
- log_info("cc-native-plan-review", "=== PHASE 4: Generate Output ===")
804
-
805
- if not cli_results and not agent_results:
806
- return skip_with_info("All reviewers failed to produce results. Check stderr logs for details.")
807
-
808
- overall = worst_verdict(all_verdicts) if all_verdicts else "pass"
809
-
810
- combined_result = CombinedReviewResult(
811
- plan_hash=plan_hash,
812
- overall_verdict=overall,
813
- cli_reviewers=cli_results,
814
- orchestration=orch_result,
815
- agents=agent_results,
816
- timestamp=datetime.now().isoformat(),
817
- )
818
-
819
- # Merge display settings from both configs
820
- display_settings = {**plan_settings.get("display", {}), **agent_settings.get("display", {})}
821
- combined_settings = {"display": display_settings}
822
-
823
- # Get current iteration number for folder naming
824
- current_iteration = 1
825
- if iteration_state:
826
- current_iteration = iteration_state.get("current", 1)
827
-
828
- # Create review folder with datetime and iteration in name
829
- review_folder = get_review_folder_path(active_context.id, current_iteration, base)
830
- review_folder.mkdir(parents=True, exist_ok=True)
831
- log_info("cc-native-plan-review", f"Created review folder: {review_folder}")
832
-
833
- review_file = write_combined_artifacts(
834
- base, plan, combined_result, payload, combined_settings,
835
- review_folder=review_folder,
836
- iteration=current_iteration,
837
- )
838
- log_info("cc-native-plan-review", f"Saved review: {review_file}")
839
-
840
- # Build inline review summary for additionalContext
841
- inline_summary = build_inline_review_summary(combined_result)
842
-
843
- context_parts = [inline_summary, f"\nFull review: `{review_file}`\n"]
844
-
845
- # Review decision — fail veto triggers a block (per-agent override already applied)
846
- warn_threshold = agent_settings.get("warnThreshold", 0.5)
847
- should_deny, deny_reason, review_score = compute_review_decision(
848
- all_verdicts, warn_threshold,
849
- )
850
-
851
- # Structured log entries for review influence tracking
852
- log_info("cc-native-plan-review", f"REVIEW_DECISION: verdict={combined_result.overall_verdict}, deny={should_deny}, reason={deny_reason}, score={review_score:.2f}")
853
- log_diagnostic("cc-native-plan-review", "result",
854
- f"verdict={combined_result.overall_verdict}, deny={should_deny}, reason={deny_reason}",
855
- decision="deny" if should_deny else "allow",
856
- reasoning=f"reason={deny_reason}, score={review_score:.2f}, warn_threshold={warn_threshold}",
857
- inputs={"overall_verdict": combined_result.overall_verdict,
858
- "review_score": round(review_score, 2),
859
- "cli_count": len(cli_results), "agent_count": len(agent_results)})
860
-
861
- # Terminal progress indicator
862
- verdict_emoji = "✅" if not should_deny else "❌"
863
- eprint(f"[plan-review] {verdict_emoji} {combined_result.overall_verdict.upper()} (score={review_score:.2f})")
864
- if should_deny:
865
- eprint(f"[plan-review] Blocking ExitPlanMode — {deny_reason}")
866
-
867
- # Handle iteration logic
868
- needs_more_iterations = False
869
- if iteration_state and reviews_dir:
870
- # Update iteration state with this review result
871
- iteration_state = update_iteration_state_in_context(reviews_dir, iteration_state, plan_hash, overall)
872
-
873
- # Check if more iterations needed
874
- if should_continue_iterating_context(iteration_state, review_score, agent_settings):
875
- needs_more_iterations = True
876
- # Increment iteration counter for next round
877
- iteration_state["current"] = iteration_state.get("current", 1) + 1
878
- # Save updated state for next iteration
879
- save_iteration_state(reviews_dir, iteration_state)
880
- else:
881
- # Final iteration - increment current and save state
882
- iteration_state["current"] = iteration_state.get("current", 1) + 1
883
- # Extend max ONLY when the plan passes review (for user rejection recovery).
884
- # When the hook denies (should_deny=True), don't extend — the hook will
885
- # keep blocking on each resubmission via should_deny regardless of max.
886
- # This prevents max from inflating on repeated hook rejections while still
887
- # allowing re-review after a user rejects a plan that passed review.
888
- if not should_deny:
889
- iteration_state["max"] = iteration_state.get("max", 1) + 1
890
- save_iteration_state(reviews_dir, iteration_state)
891
-
892
- # Emit output with correct Claude Code hook format
893
- context_text = "".join(context_parts)
894
-
895
- log_debug("cc-native-plan-review", f"REVIEW_CONTEXT_INJECTED: chars={len(context_text)}, inline_chars={len(inline_summary)}")
896
-
897
- _REVIEWER_CAVEAT = (
898
- "Reviewers have limited context compared to your full session — "
899
- "adopt valid points, use your judgment where they lack context."
900
- )
901
-
902
- _RESUBMIT_INSTRUCTION = (
903
- "IMPORTANT: After revising the plan file, you MUST call ExitPlanMode again "
904
- "to trigger re-review. Do not end your turn or ask the user without calling ExitPlanMode."
905
- )
906
-
907
- if needs_more_iterations:
908
- mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="hook_deny_iteration")
909
- current = iteration_state["current"] - 1 # Display the just-completed iteration
910
- max_iter = iteration_state["max"]
911
- remaining = max_iter - current
912
- top_issues_text = extract_top_issues_text(combined_result, max_count=3, severity="high")
913
- # Two-fold deny signal: inline issues (fallback) + high-issues.md (primary)
914
- high_issues_doc = build_high_issues_document(combined_result)
915
- high_issues_path = review_folder / "high-issues.md"
916
- high_issues_path.write_text(high_issues_doc, encoding="utf-8")
917
- emit_context_and_block(
918
- context_text,
919
- f"Plan review iteration {current}/{max_iter} FAILED ({deny_reason}, score={review_score:.2f}). "
920
- f"Critical issues: {top_issues_text}. "
921
- f"IMPORTANT: Read `{high_issues_path}` for ALL high-severity issues — "
922
- f"this file contains only the most critical findings, no noise. "
923
- f"{_REVIEWER_CAVEAT} "
924
- f"Revise the plan to address these issues, then call ExitPlanMode again. "
925
- f"({remaining} revision{'s' if remaining != 1 else ''} remaining) "
926
- f"{_RESUBMIT_INSTRUCTION}",
927
- )
928
- elif should_deny:
929
- mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="hook_deny_final")
930
- top_issues_text = extract_top_issues_text(combined_result, max_count=3, severity="high")
931
- # Two-fold deny signal: inline issues (fallback) + high-issues.md (primary)
932
- high_issues_doc = build_high_issues_document(combined_result)
933
- high_issues_path = review_folder / "high-issues.md"
934
- high_issues_path.write_text(high_issues_doc, encoding="utf-8")
935
- emit_context_and_block(
936
- context_text,
937
- f"Plan review FAILED ({deny_reason}, score={review_score:.2f}). "
938
- f"Critical issues: {top_issues_text}. "
939
- f"IMPORTANT: Read `{high_issues_path}` for ALL high-severity issues — "
940
- f"this file contains only the most critical findings, no noise. "
941
- f"{_REVIEWER_CAVEAT} "
942
- f"Revise the plan to address these issues, then call ExitPlanMode again. "
943
- f"{_RESUBMIT_INSTRUCTION}",
944
- )
945
- else:
946
- mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="allow")
947
- emit_context(context_text, ensure_ascii=True)
948
-
949
- return 0
950
-
951
-
952
- if __name__ == "__main__":
953
- from base.hook_utils import run_hook
954
- run_hook(main, "cc_native_plan_review")