aiwcli 0.9.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 (204) hide show
  1. package/README.md +1248 -0
  2. package/bin/dev.cmd +3 -0
  3. package/bin/dev.js +16 -0
  4. package/bin/run.cmd +3 -0
  5. package/bin/run.js +19 -0
  6. package/dist/commands/branch.d.ts +45 -0
  7. package/dist/commands/branch.js +488 -0
  8. package/dist/commands/clean.d.ts +34 -0
  9. package/dist/commands/clean.js +186 -0
  10. package/dist/commands/clear.d.ts +51 -0
  11. package/dist/commands/clear.js +835 -0
  12. package/dist/commands/init/index.d.ts +107 -0
  13. package/dist/commands/init/index.js +565 -0
  14. package/dist/commands/launch.d.ts +21 -0
  15. package/dist/commands/launch.js +108 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/base-command.d.ts +114 -0
  19. package/dist/lib/base-command.js +153 -0
  20. package/dist/lib/bmad-installer.d.ts +38 -0
  21. package/dist/lib/bmad-installer.js +145 -0
  22. package/dist/lib/claude-settings-types.d.ts +102 -0
  23. package/dist/lib/claude-settings-types.js +5 -0
  24. package/dist/lib/config.d.ts +25 -0
  25. package/dist/lib/config.js +46 -0
  26. package/dist/lib/debug.d.ts +39 -0
  27. package/dist/lib/debug.js +74 -0
  28. package/dist/lib/env-compat.d.ts +26 -0
  29. package/dist/lib/env-compat.js +35 -0
  30. package/dist/lib/errors.d.ts +126 -0
  31. package/dist/lib/errors.js +145 -0
  32. package/dist/lib/generic-merge.d.ts +74 -0
  33. package/dist/lib/generic-merge.js +105 -0
  34. package/dist/lib/git/branch.d.ts +67 -0
  35. package/dist/lib/git/branch.js +155 -0
  36. package/dist/lib/git/index.d.ts +11 -0
  37. package/dist/lib/git/index.js +13 -0
  38. package/dist/lib/git/safety-checks.d.ts +44 -0
  39. package/dist/lib/git/safety-checks.js +102 -0
  40. package/dist/lib/git/types.d.ts +31 -0
  41. package/dist/lib/git/types.js +6 -0
  42. package/dist/lib/git/worktree.d.ts +67 -0
  43. package/dist/lib/git/worktree.js +220 -0
  44. package/dist/lib/gitignore-manager.d.ts +10 -0
  45. package/dist/lib/gitignore-manager.js +60 -0
  46. package/dist/lib/hooks-merger.d.ts +28 -0
  47. package/dist/lib/hooks-merger.js +94 -0
  48. package/dist/lib/ide-path-resolver.d.ts +102 -0
  49. package/dist/lib/ide-path-resolver.js +129 -0
  50. package/dist/lib/index.d.ts +13 -0
  51. package/dist/lib/index.js +22 -0
  52. package/dist/lib/output.d.ts +51 -0
  53. package/dist/lib/output.js +76 -0
  54. package/dist/lib/paths.d.ts +66 -0
  55. package/dist/lib/paths.js +136 -0
  56. package/dist/lib/quiet.d.ts +12 -0
  57. package/dist/lib/quiet.js +17 -0
  58. package/dist/lib/settings-hierarchy.d.ts +42 -0
  59. package/dist/lib/settings-hierarchy.js +105 -0
  60. package/dist/lib/spawn.d.ts +105 -0
  61. package/dist/lib/spawn.js +157 -0
  62. package/dist/lib/spinner.d.ts +19 -0
  63. package/dist/lib/spinner.js +34 -0
  64. package/dist/lib/stdin.d.ts +48 -0
  65. package/dist/lib/stdin.js +60 -0
  66. package/dist/lib/template-installer.d.ts +92 -0
  67. package/dist/lib/template-installer.js +375 -0
  68. package/dist/lib/template-linter.d.ts +49 -0
  69. package/dist/lib/template-linter.js +173 -0
  70. package/dist/lib/template-merger.d.ts +47 -0
  71. package/dist/lib/template-merger.js +173 -0
  72. package/dist/lib/template-resolver.d.ts +20 -0
  73. package/dist/lib/template-resolver.js +60 -0
  74. package/dist/lib/terminal.d.ts +102 -0
  75. package/dist/lib/terminal.js +245 -0
  76. package/dist/lib/tty-detection.d.ts +62 -0
  77. package/dist/lib/tty-detection.js +83 -0
  78. package/dist/lib/user-utils.d.ts +5 -0
  79. package/dist/lib/user-utils.js +23 -0
  80. package/dist/lib/version.d.ts +99 -0
  81. package/dist/lib/version.js +144 -0
  82. package/dist/lib/watch-templates.d.ts +6 -0
  83. package/dist/lib/watch-templates.js +73 -0
  84. package/dist/lib/windsurf-hooks-hierarchy.d.ts +30 -0
  85. package/dist/lib/windsurf-hooks-hierarchy.js +66 -0
  86. package/dist/lib/windsurf-hooks-merger.d.ts +26 -0
  87. package/dist/lib/windsurf-hooks-merger.js +53 -0
  88. package/dist/lib/windsurf-hooks-types.d.ts +33 -0
  89. package/dist/lib/windsurf-hooks-types.js +5 -0
  90. package/dist/templates/CLAUDE.md +174 -0
  91. package/dist/templates/_shared/.claude/commands/handoff.md +14 -0
  92. package/dist/templates/_shared/.claude/settings.json +61 -0
  93. package/dist/templates/_shared/.codex/workflows/handoff.md +14 -0
  94. package/dist/templates/_shared/.windsurf/workflows/handoff.md +14 -0
  95. package/dist/templates/_shared/hooks/__init__.py +16 -0
  96. package/dist/templates/_shared/hooks/archive_plan.py +270 -0
  97. package/dist/templates/_shared/hooks/context_enforcer.py +621 -0
  98. package/dist/templates/_shared/hooks/context_monitor.py +322 -0
  99. package/dist/templates/_shared/hooks/file-suggestion.py +188 -0
  100. package/dist/templates/_shared/hooks/task_create_capture.py +194 -0
  101. package/dist/templates/_shared/hooks/task_update_capture.py +254 -0
  102. package/dist/templates/_shared/hooks/user_prompt_submit.py +157 -0
  103. package/dist/templates/_shared/lib/__init__.py +1 -0
  104. package/dist/templates/_shared/lib/base/__init__.py +49 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/atomic_write.py +180 -0
  107. package/dist/templates/_shared/lib/base/constants.py +299 -0
  108. package/dist/templates/_shared/lib/base/inference.py +189 -0
  109. package/dist/templates/_shared/lib/base/utils.py +216 -0
  110. package/dist/templates/_shared/lib/context/__init__.py +119 -0
  111. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  112. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  113. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  114. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  115. package/dist/templates/_shared/lib/context/cache.py +446 -0
  116. package/dist/templates/_shared/lib/context/context_manager.py +1171 -0
  117. package/dist/templates/_shared/lib/context/discovery.py +486 -0
  118. package/dist/templates/_shared/lib/context/event_log.py +308 -0
  119. package/dist/templates/_shared/lib/context/plan_archive.py +247 -0
  120. package/dist/templates/_shared/lib/context/task_sync.py +367 -0
  121. package/dist/templates/_shared/lib/handoff/__init__.py +22 -0
  122. package/dist/templates/_shared/lib/handoff/document_generator.py +307 -0
  123. package/dist/templates/_shared/lib/templates/README.md +215 -0
  124. package/dist/templates/_shared/lib/templates/__init__.py +40 -0
  125. package/dist/templates/_shared/lib/templates/formatters.py +147 -0
  126. package/dist/templates/_shared/lib/templates/plan_context.py +119 -0
  127. package/dist/templates/_shared/scripts/save_handoff.py +99 -0
  128. package/dist/templates/_shared/workflows/handoff.md +212 -0
  129. package/dist/templates/cc-native/.claude/agents/cc-native/ACCESSIBILITY-TESTER.md +80 -0
  130. package/dist/templates/cc-native/.claude/agents/cc-native/ARCHITECT-REVIEWER.md +75 -0
  131. package/dist/templates/cc-native/.claude/agents/cc-native/ASSUMPTION-CHAIN-TRACER.md +239 -0
  132. package/dist/templates/cc-native/.claude/agents/cc-native/CLARITY-AUDITOR.md +109 -0
  133. package/dist/templates/cc-native/.claude/agents/cc-native/CODE-REVIEWER.md +71 -0
  134. package/dist/templates/cc-native/.claude/agents/cc-native/COMPLETENESS-CHECKER.md +104 -0
  135. package/dist/templates/cc-native/.claude/agents/cc-native/CONTEXT-EXTRACTOR.md +93 -0
  136. package/dist/templates/cc-native/.claude/agents/cc-native/DEVILS-ADVOCATE.md +223 -0
  137. package/dist/templates/cc-native/.claude/agents/cc-native/DOCUMENTATION-REVIEWER.md +73 -0
  138. package/dist/templates/cc-native/.claude/agents/cc-native/FEASIBILITY-ANALYST.md +93 -0
  139. package/dist/templates/cc-native/.claude/agents/cc-native/FRESH-PERSPECTIVE.md +103 -0
  140. package/dist/templates/cc-native/.claude/agents/cc-native/HANDOFF-READINESS.md +145 -0
  141. package/dist/templates/cc-native/.claude/agents/cc-native/HIDDEN-COMPLEXITY-DETECTOR.md +248 -0
  142. package/dist/templates/cc-native/.claude/agents/cc-native/INCENTIVE-MAPPER.md +235 -0
  143. package/dist/templates/cc-native/.claude/agents/cc-native/PENETRATION-TESTER.md +80 -0
  144. package/dist/templates/cc-native/.claude/agents/cc-native/PERFORMANCE-ENGINEER.md +76 -0
  145. package/dist/templates/cc-native/.claude/agents/cc-native/PLAN-ORCHESTRATOR.md +141 -0
  146. package/dist/templates/cc-native/.claude/agents/cc-native/PRECEDENT-FINDER.md +240 -0
  147. package/dist/templates/cc-native/.claude/agents/cc-native/REVERSIBILITY-ANALYST.md +211 -0
  148. package/dist/templates/cc-native/.claude/agents/cc-native/RISK-ASSESSOR.md +101 -0
  149. package/dist/templates/cc-native/.claude/agents/cc-native/SECOND-ORDER-ANALYST.md +197 -0
  150. package/dist/templates/cc-native/.claude/agents/cc-native/SIMPLICITY-GUARDIAN.md +97 -0
  151. package/dist/templates/cc-native/.claude/agents/cc-native/SKEPTIC.md +349 -0
  152. package/dist/templates/cc-native/.claude/agents/cc-native/STAKEHOLDER-ADVOCATE.md +106 -0
  153. package/dist/templates/cc-native/.claude/agents/cc-native/TRADE-OFF-ILLUMINATOR.md +205 -0
  154. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +8 -0
  155. package/dist/templates/cc-native/.claude/commands/cc-native/specdev.md +10 -0
  156. package/dist/templates/cc-native/.claude/settings.json +119 -0
  157. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fix.md +8 -0
  158. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +8 -0
  159. package/dist/templates/cc-native/.windsurf/workflows/cc-native/implement.md +8 -0
  160. package/dist/templates/cc-native/.windsurf/workflows/cc-native/research.md +8 -0
  161. package/dist/templates/cc-native/CC-NATIVE-README.md +192 -0
  162. package/dist/templates/cc-native/MIGRATION.md +86 -0
  163. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +331 -0
  164. package/dist/templates/cc-native/_cc-native/docs/PERMISSION_REQUEST_VERIFICATION.md +147 -0
  165. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +150 -0
  171. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +746 -0
  172. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +339 -0
  173. package/dist/templates/cc-native/_cc-native/lib/__init__.py +57 -0
  174. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  175. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  176. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +68 -0
  179. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +98 -0
  180. package/dist/templates/cc-native/_cc-native/lib/constants.py +45 -0
  181. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +273 -0
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +28 -0
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  185. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  186. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  187. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +164 -0
  189. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +89 -0
  190. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +119 -0
  191. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +103 -0
  192. package/dist/templates/cc-native/_cc-native/lib/state.py +251 -0
  193. package/dist/templates/cc-native/_cc-native/lib/utils.py +830 -0
  194. package/dist/templates/cc-native/_cc-native/plan-review.config.json +76 -0
  195. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  196. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +151 -0
  197. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +134 -0
  198. package/dist/templates/cc-native/_cc-native/workflows/specdev.md +9 -0
  199. package/dist/types/exit-codes.d.ts +11 -0
  200. package/dist/types/exit-codes.js +10 -0
  201. package/dist/types/index.d.ts +5 -0
  202. package/dist/types/index.js +7 -0
  203. package/oclif.manifest.json +405 -0
  204. package/package.json +109 -0
@@ -0,0 +1,746 @@
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 sys
32
+ from concurrent.futures import ThreadPoolExecutor, as_completed
33
+ from datetime import datetime
34
+ from pathlib import Path
35
+ from typing import Any, Dict, List, Optional
36
+
37
+ # Import shared library
38
+ try:
39
+ _lib = Path(__file__).parent.parent / "lib"
40
+ sys.path.insert(0, str(_lib))
41
+
42
+ # Add shared library path
43
+ _shared = Path(__file__).parent.parent.parent / "_shared"
44
+ sys.path.insert(0, str(_shared))
45
+
46
+ from utils import (
47
+ DEFAULT_DISPLAY,
48
+ DEFAULT_SANITIZATION,
49
+ REVIEW_SCHEMA,
50
+ ReviewerResult,
51
+ CombinedReviewResult,
52
+ eprint,
53
+ project_dir,
54
+ find_plan_file,
55
+ compute_plan_hash,
56
+ is_plan_already_reviewed,
57
+ mark_plan_reviewed,
58
+ worst_verdict,
59
+ format_combined_markdown,
60
+ write_combined_artifacts,
61
+ load_config,
62
+ get_display_settings,
63
+ )
64
+ from reviewers import (
65
+ run_codex_review,
66
+ run_gemini_review,
67
+ run_agent_review,
68
+ AgentConfig,
69
+ OrchestratorConfig,
70
+ )
71
+ from orchestrator import (
72
+ run_orchestrator,
73
+ DEFAULT_AGENT_SELECTION,
74
+ DEFAULT_COMPLEXITY_CATEGORIES,
75
+ )
76
+ # Import shared context system
77
+ from lib.context.context_manager import (
78
+ get_context_by_session_id,
79
+ get_all_in_flight_contexts,
80
+ get_all_contexts,
81
+ )
82
+ from lib.base.constants import get_context_reviews_dir
83
+ except ImportError as e:
84
+ print(f"[cc-native-plan-review] Failed to import lib: {e}", file=sys.stderr)
85
+ sys.exit(0) # Non-blocking failure
86
+
87
+ # Add scripts directory to path for aggregate_agents import
88
+ _scripts_dir = Path(__file__).parent.parent / "scripts"
89
+ if str(_scripts_dir) not in sys.path:
90
+ sys.path.insert(0, str(_scripts_dir))
91
+
92
+ try:
93
+ from aggregate_agents import aggregate_agents
94
+ except ImportError:
95
+ def aggregate_agents(agents_dir: Path) -> List[Dict[str, Any]]:
96
+ eprint("[cc-native-plan-review] Warning: aggregate_agents not found")
97
+ return []
98
+
99
+
100
+ # ---------------------------
101
+ # Default Configuration
102
+ # ---------------------------
103
+
104
+ DEFAULT_AGENTS: List[Dict[str, Any]] = [
105
+ {"name": "architect-reviewer", "model": "sonnet", "focus": "architectural concerns and scalability", "enabled": True, "categories": ["code", "infrastructure", "design"]},
106
+ {"name": "penetration-tester", "model": "sonnet", "focus": "security vulnerabilities and attack vectors", "enabled": True, "categories": ["code", "infrastructure"]},
107
+ {"name": "performance-engineer", "model": "sonnet", "focus": "performance bottlenecks and optimization", "enabled": True, "categories": ["code", "infrastructure"]},
108
+ {"name": "accessibility-tester", "model": "sonnet", "focus": "accessibility compliance and UX concerns", "enabled": True, "categories": ["code", "design"]},
109
+ ]
110
+
111
+ DEFAULT_ORCHESTRATOR: Dict[str, Any] = {
112
+ "enabled": True,
113
+ "model": "haiku",
114
+ "timeout": 30,
115
+ "maxTurns": 3,
116
+ }
117
+
118
+ DEFAULT_AGENT_MODEL: str = "sonnet"
119
+
120
+ DEFAULT_REVIEW_ITERATIONS: Dict[str, int] = {
121
+ "simple": 1,
122
+ "medium": 1,
123
+ "high": 2,
124
+ }
125
+
126
+
127
+ # ---------------------------
128
+ # Context-based State Management
129
+ # ---------------------------
130
+
131
+ def get_active_context_for_review(session_id: str, project_root: Path) -> Optional[Any]:
132
+ """Find active context for plan review.
133
+
134
+ Strategy:
135
+ 1. Find context by session_id
136
+ 2. Fallback: Single in-flight context
137
+ 3. Fallback: Single planning context
138
+ 4. Return None if multiple or no contexts found
139
+
140
+ Args:
141
+ session_id: Current session ID
142
+ project_root: Project root path
143
+
144
+ Returns:
145
+ Context object or None
146
+ """
147
+ # Strategy 1: Find by session_id
148
+ context = get_context_by_session_id(session_id, project_root)
149
+ if context:
150
+ eprint(f"[cc-native-plan-review] Found context by session_id: {context.id}")
151
+ return context
152
+
153
+ # Strategy 2: Single in-flight context
154
+ in_flight = get_all_in_flight_contexts(project_root)
155
+ if len(in_flight) == 1:
156
+ eprint(f"[cc-native-plan-review] Found single in-flight context: {in_flight[0].id}")
157
+ return in_flight[0]
158
+
159
+ # Strategy 3: Single planning context
160
+ planning_contexts = [c for c in in_flight if c.in_flight and c.in_flight.mode == "planning"]
161
+ if len(planning_contexts) == 1:
162
+ eprint(f"[cc-native-plan-review] Found single planning context: {planning_contexts[0].id}")
163
+ return planning_contexts[0]
164
+
165
+ # Multiple or no contexts found
166
+ if len(in_flight) > 1:
167
+ eprint(f"[cc-native-plan-review] Multiple in-flight contexts ({len(in_flight)}), falling back to legacy")
168
+ else:
169
+ eprint("[cc-native-plan-review] No in-flight contexts found, falling back to legacy")
170
+ return None
171
+
172
+
173
+ def load_iteration_state(reviews_dir: Path) -> Optional[Dict[str, Any]]:
174
+ """Load iteration state from context reviews folder.
175
+
176
+ Args:
177
+ reviews_dir: Path to the reviews directory
178
+
179
+ Returns:
180
+ Iteration state dict or None if not found
181
+ """
182
+ iteration_file = reviews_dir / "iteration.json"
183
+ if not iteration_file.exists():
184
+ return None
185
+
186
+ try:
187
+ return json.loads(iteration_file.read_text(encoding="utf-8"))
188
+ except Exception as e:
189
+ eprint(f"[cc-native-plan-review] Failed to load iteration state: {e}")
190
+ return None
191
+
192
+
193
+ def save_iteration_state(reviews_dir: Path, state: Dict[str, Any]) -> bool:
194
+ """Save iteration state to context reviews folder.
195
+
196
+ Args:
197
+ reviews_dir: Path to the reviews directory
198
+ state: Iteration state dict
199
+
200
+ Returns:
201
+ True on success, False on failure
202
+ """
203
+ iteration_file = reviews_dir / "iteration.json"
204
+ try:
205
+ reviews_dir.mkdir(parents=True, exist_ok=True)
206
+ state["schema_version"] = "1.0.0"
207
+ iteration_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
208
+ return True
209
+ except Exception as e:
210
+ eprint(f"[cc-native-plan-review] Failed to save iteration state: {e}")
211
+ return False
212
+
213
+
214
+ def get_iteration_state_from_context(
215
+ reviews_dir: Path,
216
+ complexity: str,
217
+ config: Optional[Dict[str, Any]] = None,
218
+ ) -> Dict[str, Any]:
219
+ """Get or initialize iteration state based on complexity.
220
+
221
+ Args:
222
+ reviews_dir: Path to the reviews directory
223
+ complexity: Plan complexity level (simple/medium/high)
224
+ config: Optional config dict with reviewIterations settings
225
+
226
+ Returns:
227
+ Iteration dict with: current, max, complexity, history
228
+ """
229
+ existing = load_iteration_state(reviews_dir)
230
+ if existing:
231
+ return existing
232
+
233
+ # Initialize new iteration state
234
+ review_iterations = DEFAULT_REVIEW_ITERATIONS.copy()
235
+ if config:
236
+ review_iterations.update(config.get("reviewIterations", {}))
237
+ max_iterations = review_iterations.get(complexity, 1)
238
+
239
+ return {
240
+ "current": 1,
241
+ "max": max_iterations,
242
+ "complexity": complexity,
243
+ "history": [],
244
+ }
245
+
246
+
247
+ def update_iteration_state_in_context(
248
+ reviews_dir: Path,
249
+ iteration: Dict[str, Any],
250
+ plan_hash: str,
251
+ verdict: str,
252
+ ) -> Dict[str, Any]:
253
+ """Record review result in iteration history.
254
+
255
+ Args:
256
+ reviews_dir: Path to the reviews directory
257
+ iteration: The iteration state dict
258
+ plan_hash: Hash of the current plan content
259
+ verdict: Review verdict (pass/warn/fail)
260
+
261
+ Returns:
262
+ Updated iteration state dict
263
+ """
264
+ from datetime import datetime
265
+
266
+ iteration["history"].append({
267
+ "hash": plan_hash,
268
+ "verdict": verdict,
269
+ "timestamp": datetime.now().isoformat(),
270
+ })
271
+ return iteration
272
+
273
+
274
+ def should_continue_iterating_context(
275
+ iteration: Dict[str, Any],
276
+ verdict: str,
277
+ config: Optional[Dict[str, Any]] = None,
278
+ ) -> bool:
279
+ """Determine if more review iterations are needed.
280
+
281
+ Args:
282
+ iteration: The iteration state dict
283
+ verdict: Current review verdict
284
+ config: Optional config dict with earlyExitOnAllPass setting
285
+
286
+ Returns:
287
+ True if more iterations needed, False otherwise
288
+ """
289
+ current = iteration.get("current", 1)
290
+ max_iter = iteration.get("max", 1)
291
+
292
+ # At or past max iterations - no more iterations
293
+ if current >= max_iter:
294
+ eprint(f"[cc-native-plan-review] At max iterations ({current}/{max_iter}), no more iterations")
295
+ return False
296
+
297
+ # Check early exit on all pass
298
+ early_exit = True
299
+ if config:
300
+ early_exit = config.get("earlyExitOnAllPass", True)
301
+ if early_exit and verdict == "pass":
302
+ eprint(f"[cc-native-plan-review] All reviewers passed and earlyExitOnAllPass=true, exiting early")
303
+ return False
304
+
305
+ # More iterations available and verdict is not pass (or early exit disabled)
306
+ eprint(f"[cc-native-plan-review] Continuing to next iteration ({current + 1}/{max_iter}), verdict={verdict}")
307
+ return True
308
+
309
+
310
+ # ---------------------------
311
+ # Settings Loading
312
+ # ---------------------------
313
+
314
+ def load_settings(proj_dir: Path) -> Dict[str, Any]:
315
+ """Load CC-Native settings from _cc-native/plan-review.config.json"""
316
+ defaults = {
317
+ "planReview": {
318
+ "enabled": True,
319
+ "reviewers": {
320
+ "codex": {"enabled": True, "model": "", "timeout": 120},
321
+ "gemini": {"enabled": False, "model": "", "timeout": 120},
322
+ },
323
+ "blockOnFail": False,
324
+ "display": DEFAULT_DISPLAY.copy(),
325
+ },
326
+ "agentReview": {
327
+ "enabled": True,
328
+ "orchestrator": DEFAULT_ORCHESTRATOR.copy(),
329
+ "timeout": 120,
330
+ "blockOnFail": True,
331
+ "legacyMode": False,
332
+ "maxTurns": 3,
333
+ "display": DEFAULT_DISPLAY.copy(),
334
+ "agentSelection": DEFAULT_AGENT_SELECTION.copy(),
335
+ "agentDefaults": {"model": DEFAULT_AGENT_MODEL},
336
+ "complexityCategories": DEFAULT_COMPLEXITY_CATEGORIES.copy(),
337
+ "sanitization": DEFAULT_SANITIZATION.copy(),
338
+ },
339
+ }
340
+
341
+ config = load_config(proj_dir)
342
+ if not config:
343
+ return defaults
344
+
345
+ # Merge planReview settings
346
+ plan_review = config.get("planReview", {})
347
+ merged_plan = defaults["planReview"].copy()
348
+ merged_plan.update(plan_review)
349
+ if "reviewers" in plan_review:
350
+ merged_plan["reviewers"] = defaults["planReview"]["reviewers"].copy()
351
+ merged_plan["reviewers"].update(plan_review["reviewers"])
352
+ merged_plan["display"] = get_display_settings(config, "planReview")
353
+
354
+ # Merge agentReview settings
355
+ agent_review = config.get("agentReview", {})
356
+ merged_agent = defaults["agentReview"].copy()
357
+ merged_agent.update(agent_review)
358
+
359
+ # Handle orchestrator nested config
360
+ if "orchestrator" not in merged_agent or not isinstance(merged_agent["orchestrator"], dict):
361
+ merged_agent["orchestrator"] = DEFAULT_ORCHESTRATOR.copy()
362
+ else:
363
+ orch = DEFAULT_ORCHESTRATOR.copy()
364
+ orch.update(merged_agent["orchestrator"])
365
+ merged_agent["orchestrator"] = orch
366
+
367
+ merged_agent["display"] = get_display_settings(config, "agentReview")
368
+ merged_agent["agentSelection"] = {**DEFAULT_AGENT_SELECTION, **config.get("agentSelection", {})}
369
+ merged_agent["agentDefaults"] = {**{"model": DEFAULT_AGENT_MODEL}, **config.get("agentDefaults", {})}
370
+ merged_agent["complexityCategories"] = config.get("complexityCategories", DEFAULT_COMPLEXITY_CATEGORIES.copy())
371
+ merged_agent["sanitization"] = {**DEFAULT_SANITIZATION, **config.get("sanitization", {})}
372
+
373
+ # Merge reviewIterations settings
374
+ merged_agent["reviewIterations"] = {**DEFAULT_REVIEW_ITERATIONS, **agent_review.get("reviewIterations", {})}
375
+ merged_agent["earlyExitOnAllPass"] = agent_review.get("earlyExitOnAllPass", True)
376
+
377
+ return {"planReview": merged_plan, "agentReview": merged_agent}
378
+
379
+
380
+ def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None) -> List[AgentConfig]:
381
+ """Load agent library by auto-detecting from frontmatter."""
382
+ agents_dir = proj_dir / ".claude" / "agents" / "cc-native"
383
+ agents_data = aggregate_agents(agents_dir)
384
+
385
+ default_model = DEFAULT_AGENT_MODEL
386
+ if settings:
387
+ default_model = settings.get("agentDefaults", {}).get("model", DEFAULT_AGENT_MODEL)
388
+
389
+ if not agents_data:
390
+ eprint("[cc-native-plan-review] No agents found in frontmatter, using defaults")
391
+ return [
392
+ AgentConfig(
393
+ name=a["name"],
394
+ model=a.get("model", default_model),
395
+ focus=a.get("focus", "general review"),
396
+ enabled=a.get("enabled", True),
397
+ categories=a.get("categories", ["code"]),
398
+ )
399
+ for a in DEFAULT_AGENTS
400
+ ]
401
+
402
+ agents = []
403
+ for a in agents_data:
404
+ if a.get("name") == "plan-orchestrator":
405
+ continue
406
+ agents.append(AgentConfig(
407
+ name=a["name"],
408
+ model=a.get("model", default_model),
409
+ focus=a.get("focus", "general review"),
410
+ enabled=a.get("enabled", True),
411
+ categories=a.get("categories", ["code"]),
412
+ description=a.get("description", ""),
413
+ tools=a.get("tools", ""),
414
+ ))
415
+
416
+ return agents
417
+
418
+
419
+ # ---------------------------
420
+ # Main Hook
421
+ # ---------------------------
422
+
423
+ def main() -> int:
424
+ eprint("[cc-native-plan-review] Unified hook started (PreToolUse)")
425
+
426
+ try:
427
+ payload = json.load(sys.stdin)
428
+ except json.JSONDecodeError as e:
429
+ eprint(f"[cc-native-plan-review] Invalid JSON input: {e}")
430
+ return 0
431
+
432
+ tool_name = payload.get("tool_name")
433
+ eprint(f"[cc-native-plan-review] tool_name: {tool_name}")
434
+
435
+ # Only process ExitPlanMode
436
+ if tool_name != "ExitPlanMode":
437
+ eprint("[cc-native-plan-review] Skipping: not ExitPlanMode")
438
+ return 0
439
+
440
+ session_id = str(payload.get("session_id", "unknown"))
441
+ base = project_dir(payload)
442
+ settings = load_settings(base)
443
+
444
+ plan_settings = settings.get("planReview", {})
445
+ agent_settings = settings.get("agentReview", {})
446
+
447
+ plan_review_enabled = plan_settings.get("enabled", True)
448
+ agent_review_enabled = agent_settings.get("enabled", True)
449
+
450
+ if not plan_review_enabled and not agent_review_enabled:
451
+ eprint("[cc-native-plan-review] Skipping: both plan and agent review disabled")
452
+ return 0
453
+
454
+ # Find and read plan FIRST (state file is keyed by plan path)
455
+ plan_path = find_plan_file()
456
+ if not plan_path:
457
+ eprint("[cc-native-plan-review] Skipping: no plan file found in ~/.claude/plans/")
458
+ return 0
459
+
460
+ try:
461
+ plan = Path(plan_path).read_text(encoding="utf-8").strip()
462
+ except Exception as e:
463
+ eprint(f"[cc-native-plan-review] Failed to read plan file: {e}")
464
+ return 0
465
+
466
+ if not plan:
467
+ eprint("[cc-native-plan-review] Skipping: plan file is empty")
468
+ return 0
469
+
470
+ eprint(f"[cc-native-plan-review] Found plan at: {plan_path}")
471
+ eprint(f"[cc-native-plan-review] Plan length: {len(plan)} chars")
472
+
473
+ # Find active context for this review (required)
474
+ active_context = get_active_context_for_review(session_id, base)
475
+
476
+ if not active_context:
477
+ eprint("[cc-native-plan-review] Skipping: no active context found")
478
+ return 0
479
+
480
+ # Get base reviews dir from shared lib, then add cc-native namespace
481
+ reviews_dir = get_context_reviews_dir(active_context.id, base) / "cc-native"
482
+ eprint(f"[cc-native-plan-review] Using context reviews dir: {reviews_dir}")
483
+
484
+ # Check if we've exhausted review iterations from context
485
+ existing_iteration = load_iteration_state(reviews_dir)
486
+ if existing_iteration:
487
+ current = existing_iteration.get("current", 1)
488
+ max_iter = existing_iteration.get("max", 1)
489
+ if current > max_iter:
490
+ eprint(f"[cc-native-plan-review] Skipping: review iterations exhausted ({current}/{max_iter})")
491
+ return 0
492
+
493
+ # Plan-hash deduplication
494
+ plan_hash = compute_plan_hash(plan)
495
+ eprint(f"[cc-native-plan-review] Plan hash: {plan_hash}")
496
+ if is_plan_already_reviewed(session_id, plan_hash):
497
+ eprint("[cc-native-plan-review] Skipping: plan already reviewed (hash match)")
498
+ return 0
499
+
500
+ # Initialize combined result
501
+ cli_results: Dict[str, ReviewerResult] = {}
502
+ orch_result = None
503
+ agent_results: Dict[str, ReviewerResult] = {}
504
+ all_verdicts: List[str] = []
505
+ iteration_state: Optional[Dict[str, Any]] = None
506
+ detected_complexity: str = "medium" # Will be updated by orchestrator
507
+
508
+ # ============================================
509
+ # PHASE 1 & 2: CLI Reviewers + Orchestrator (PARALLEL)
510
+ # ============================================
511
+ # Run CLI reviewers and orchestrator concurrently for speed
512
+ reviewers_config = plan_settings.get("reviewers", {}) if plan_review_enabled else {}
513
+ codex_enabled = plan_review_enabled and reviewers_config.get("codex", {}).get("enabled", True)
514
+ gemini_enabled = plan_review_enabled and reviewers_config.get("gemini", {}).get("enabled", False)
515
+
516
+ agent_library = load_agent_library(base, agent_settings) if agent_review_enabled else []
517
+ enabled_agents = [a for a in agent_library if a.enabled]
518
+ timeout = agent_settings.get("timeout", 120)
519
+ legacy_mode = agent_settings.get("legacyMode", False)
520
+
521
+ orch_settings = agent_settings.get("orchestrator", DEFAULT_ORCHESTRATOR)
522
+ orchestrator_config = OrchestratorConfig(
523
+ enabled=orch_settings.get("enabled", True) and agent_review_enabled,
524
+ model=orch_settings.get("model", "haiku"),
525
+ timeout=orch_settings.get("timeout", 30),
526
+ max_turns=orch_settings.get("maxTurns", 3),
527
+ )
528
+
529
+ eprint(f"[cc-native-plan-review] Codex enabled: {codex_enabled}, Gemini enabled: {gemini_enabled}")
530
+ eprint(f"[cc-native-plan-review] Agent library: {[a.name for a in agent_library]}")
531
+ eprint(f"[cc-native-plan-review] Enabled agents: {[a.name for a in enabled_agents]}")
532
+ eprint(f"[cc-native-plan-review] Orchestrator enabled: {orchestrator_config.enabled}")
533
+
534
+ # Run CLI reviewers + orchestrator in parallel
535
+ phase1_tasks = []
536
+ if codex_enabled:
537
+ phase1_tasks.append(("codex", lambda: run_codex_review(plan, REVIEW_SCHEMA, plan_settings)))
538
+ if gemini_enabled:
539
+ phase1_tasks.append(("gemini", lambda: run_gemini_review(plan, REVIEW_SCHEMA, plan_settings)))
540
+ if orchestrator_config.enabled and enabled_agents and not legacy_mode:
541
+ phase1_tasks.append(("orchestrator", lambda: run_orchestrator(plan, enabled_agents, orchestrator_config, agent_settings)))
542
+
543
+ eprint(f"[cc-native-plan-review] === PHASE 1: Running {len(phase1_tasks)} tasks in parallel ===")
544
+
545
+ phase1_results: Dict[str, Any] = {}
546
+ if phase1_tasks:
547
+ with ThreadPoolExecutor(max_workers=len(phase1_tasks)) as executor:
548
+ futures = {executor.submit(task_fn): name for name, task_fn in phase1_tasks}
549
+ for future in as_completed(futures):
550
+ name = futures[future]
551
+ try:
552
+ phase1_results[name] = future.result()
553
+ eprint(f"[cc-native-plan-review] {name} completed")
554
+ except Exception as ex:
555
+ eprint(f"[cc-native-plan-review] {name} failed: {ex}")
556
+ phase1_results[name] = None
557
+
558
+ # Collect CLI results
559
+ if "codex" in phase1_results and phase1_results["codex"]:
560
+ cli_results["codex"] = phase1_results["codex"]
561
+ if phase1_results["codex"].verdict and phase1_results["codex"].verdict not in ("skip", "error"):
562
+ all_verdicts.append(phase1_results["codex"].verdict)
563
+ if "gemini" in phase1_results and phase1_results["gemini"]:
564
+ cli_results["gemini"] = phase1_results["gemini"]
565
+ if phase1_results["gemini"].verdict and phase1_results["gemini"].verdict not in ("skip", "error"):
566
+ all_verdicts.append(phase1_results["gemini"].verdict)
567
+
568
+ # Get orchestrator result
569
+ if "orchestrator" in phase1_results and phase1_results["orchestrator"]:
570
+ orch_result = phase1_results["orchestrator"]
571
+
572
+ # ============================================
573
+ # PHASE 2: Agent Selection (from orchestrator result)
574
+ # ============================================
575
+ if agent_review_enabled:
576
+ eprint("[cc-native-plan-review] === PHASE 2: Agent Selection ===")
577
+
578
+ selected_agents: List[AgentConfig] = []
579
+
580
+ if enabled_agents:
581
+ if orch_result and not legacy_mode:
582
+ # Use orchestrator result from phase 1
583
+ detected_complexity = orch_result.complexity
584
+
585
+ if orch_result.complexity == "simple" and not orch_result.selected_agents:
586
+ eprint("[cc-native-plan-review] Orchestrator determined: simple complexity, no agent review needed")
587
+ else:
588
+ selected_names = set(orch_result.selected_agents)
589
+ selected_agents = [a for a in enabled_agents if a.name in selected_names]
590
+
591
+ if not selected_agents and selected_names:
592
+ eprint(f"[cc-native-plan-review] Warning: orchestrator selected unknown agents: {selected_names}")
593
+ selected_agents = [a for a in enabled_agents if orch_result.category in a.categories]
594
+
595
+ eprint(f"[cc-native-plan-review] Orchestrator selected: {[a.name for a in selected_agents]}")
596
+ else:
597
+ eprint("[cc-native-plan-review] Running in legacy mode (all enabled agents)")
598
+ selected_agents = enabled_agents
599
+ detected_complexity = "medium" # Default for legacy mode
600
+
601
+ # Initialize iteration state based on complexity (after orchestrator runs)
602
+ if reviews_dir:
603
+ iteration_state = get_iteration_state_from_context(reviews_dir, detected_complexity, agent_settings)
604
+ eprint(f"[cc-native-plan-review] Iteration state: {iteration_state['current']}/{iteration_state['max']} ({detected_complexity})")
605
+
606
+ # PHASE 3: Run selected agents in parallel
607
+ if selected_agents:
608
+ eprint("[cc-native-plan-review] === PHASE 3: Agent Reviews ===")
609
+ max_turns = agent_settings.get("maxTurns", 3)
610
+ max_parallel = agent_settings.get("maxParallelAgents", 0) # 0 = unlimited
611
+ num_workers = len(selected_agents) if max_parallel <= 0 else min(max_parallel, len(selected_agents))
612
+ eprint(f"[cc-native-plan-review] Launching {len(selected_agents)} agents in parallel (workers={num_workers})")
613
+
614
+ with ThreadPoolExecutor(max_workers=num_workers) as executor:
615
+ futures = {
616
+ executor.submit(run_agent_review, plan, agent, REVIEW_SCHEMA, timeout, max_turns): agent
617
+ for agent in selected_agents
618
+ }
619
+ for future in as_completed(futures):
620
+ agent = futures[future]
621
+ try:
622
+ result = future.result()
623
+ agent_results[agent.name] = result
624
+ if result.verdict and result.verdict not in ("skip", "error"):
625
+ all_verdicts.append(result.verdict)
626
+ eprint(f"[cc-native-plan-review] {agent.name} completed with verdict: {result.verdict}")
627
+ except Exception as ex:
628
+ eprint(f"[cc-native-plan-review] {agent.name} failed with exception: {ex}")
629
+ agent_results[agent.name] = ReviewerResult(
630
+ name=agent.name,
631
+ ok=False,
632
+ verdict="error",
633
+ data={},
634
+ raw="",
635
+ err=str(ex),
636
+ )
637
+
638
+ # ============================================
639
+ # PHASE 4: Generate Combined Output
640
+ # ============================================
641
+ eprint("[cc-native-plan-review] === PHASE 4: Generate Output ===")
642
+
643
+ if not cli_results and not agent_results:
644
+ eprint("[cc-native-plan-review] No review results, exiting")
645
+ return 0
646
+
647
+ overall = worst_verdict(all_verdicts) if all_verdicts else "pass"
648
+
649
+ combined_result = CombinedReviewResult(
650
+ plan_hash=plan_hash,
651
+ overall_verdict=overall,
652
+ cli_reviewers=cli_results,
653
+ orchestration=orch_result,
654
+ agents=agent_results,
655
+ timestamp=datetime.now().isoformat(),
656
+ )
657
+
658
+ # Merge display settings from both configs
659
+ display_settings = {**plan_settings.get("display", {}), **agent_settings.get("display", {})}
660
+ combined_settings = {"display": display_settings}
661
+
662
+ review_file = write_combined_artifacts(
663
+ base, plan, combined_result, payload, combined_settings,
664
+ context_reviews_dir=reviews_dir
665
+ )
666
+ eprint(f"[cc-native-plan-review] Saved review: {review_file}")
667
+
668
+ # Build context message
669
+ md_content = format_combined_markdown(combined_result, combined_settings)
670
+
671
+ context_parts = [
672
+ "**CC-Native Plan Review Complete**\n\n",
673
+ f"Review saved to: `{review_file}`\n\n",
674
+ ]
675
+
676
+ if cli_results:
677
+ cli_verdicts = [f"{name}={r.verdict}" for name, r in cli_results.items()]
678
+ context_parts.append(f"**CLI Reviewers:** {', '.join(cli_verdicts)}\n")
679
+
680
+ if orch_result:
681
+ context_parts.append(f"**Orchestration:** Complexity=`{orch_result.complexity}`, Category=`{orch_result.category}`, Agents selected: {len(agent_results)}\n")
682
+
683
+ context_parts.append("\nUse these findings before starting implementation.\n\n")
684
+ context_parts.append(md_content)
685
+
686
+ # Check blocking conditions
687
+ block_on_fail_plan = plan_settings.get("blockOnFail", False)
688
+ block_on_fail_agent = agent_settings.get("blockOnFail", True)
689
+ should_block = (overall == "fail") and (block_on_fail_plan or block_on_fail_agent)
690
+
691
+ # Handle iteration logic
692
+ needs_more_iterations = False
693
+ if iteration_state and reviews_dir:
694
+ # Update iteration state with this review result
695
+ iteration_state = update_iteration_state_in_context(reviews_dir, iteration_state, plan_hash, overall)
696
+
697
+ # Check if more iterations needed
698
+ if should_continue_iterating_context(iteration_state, overall, agent_settings):
699
+ needs_more_iterations = True
700
+ # Increment iteration counter for next round
701
+ iteration_state["current"] = iteration_state.get("current", 1) + 1
702
+ # Save updated state for next iteration
703
+ save_iteration_state(reviews_dir, iteration_state)
704
+ else:
705
+ # Final iteration - increment current and save state
706
+ iteration_state["current"] = iteration_state.get("current", 1) + 1
707
+ save_iteration_state(reviews_dir, iteration_state)
708
+
709
+ # Build output with correct Claude Code hook format
710
+ # See: https://docs.anthropic.com/en/docs/claude-code/hooks
711
+ out: Dict[str, Any] = {
712
+ "hookSpecificOutput": {
713
+ "hookEventName": "PreToolUse",
714
+ "additionalContext": "".join(context_parts),
715
+ }
716
+ }
717
+
718
+ # Handle blocking scenarios - use permissionDecision/permissionDecisionReason inside hookSpecificOutput
719
+ # Note: md_content is already in additionalContext, so permissionDecisionReason only needs the instruction
720
+ if needs_more_iterations:
721
+ current = iteration_state["current"] - 1 # Display the just-completed iteration
722
+ max_iter = iteration_state["max"]
723
+ remaining = max_iter - current
724
+
725
+ out["hookSpecificOutput"]["permissionDecision"] = "deny"
726
+ out["hookSpecificOutput"]["permissionDecisionReason"] = (
727
+ f"CC-Native plan review iteration {current}/{max_iter} verdict = {overall.upper()}. "
728
+ f"REVISION REQUIRED: Address the issues in additionalContext. "
729
+ f"Revise the plan in place, then attempt ExitPlanMode again. "
730
+ f"({remaining} revision{'s' if remaining != 1 else ''} remaining)"
731
+ )
732
+ elif should_block:
733
+ out["hookSpecificOutput"]["permissionDecision"] = "deny"
734
+ out["hookSpecificOutput"]["permissionDecisionReason"] = (
735
+ "CC-Native plan review verdict = FAIL. Do NOT start implementation yet. "
736
+ "Revise the plan to address the issues in additionalContext, "
737
+ "then attempt ExitPlanMode again."
738
+ )
739
+
740
+ mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state)
741
+ print(json.dumps(out, ensure_ascii=False))
742
+ return 0
743
+
744
+
745
+ if __name__ == "__main__":
746
+ raise SystemExit(main())