aiwcli 0.9.7 → 0.10.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 (119) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +49 -18
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -45,8 +45,10 @@ try:
45
45
  _shared = Path(__file__).parent.parent.parent / "_shared"
46
46
  sys.path.insert(0, str(_shared))
47
47
 
48
- # Import subprocess utilities
48
+ # Import subprocess and hook utilities
49
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
50
52
 
51
53
  from utils import (
52
54
  DEFAULT_DISPLAY,
@@ -54,15 +56,19 @@ try:
54
56
  REVIEW_SCHEMA,
55
57
  ReviewerResult,
56
58
  CombinedReviewResult,
57
- eprint,
58
59
  project_dir,
60
+ eprint,
59
61
  find_plan_file,
60
62
  compute_plan_hash,
63
+ compute_review_decision,
61
64
  is_plan_already_reviewed,
65
+ was_plan_previously_denied,
62
66
  mark_plan_reviewed,
63
67
  worst_verdict,
64
68
  format_combined_markdown,
65
69
  write_combined_artifacts,
70
+ build_inline_review_summary,
71
+ extract_top_issues_text,
66
72
  load_config,
67
73
  get_display_settings,
68
74
  )
@@ -79,15 +85,23 @@ try:
79
85
  DEFAULT_COMPLEXITY_CATEGORIES,
80
86
  )
81
87
  # Import shared context system
82
- from lib.context.context_manager import (
88
+ from lib.context.context_store import (
83
89
  get_context_by_session_id,
84
- get_all_in_flight_contexts,
85
90
  get_all_contexts,
86
91
  )
87
92
  from lib.base.constants import get_context_reviews_dir, get_review_folder_path, get_context_dir
88
93
  from debug import debug_log, debug_raw
89
94
  except ImportError as e:
90
- print(f"[cc-native-plan-review] Failed to import lib: {e}", file=sys.stderr)
95
+ try:
96
+ from lib.base.logger import log_error as _early_log_error
97
+ _early_log_error("cc-native-plan-review", f"Failed to import lib: {e}")
98
+ except Exception:
99
+ print(f"[cc-native-plan-review] Failed to import lib: {e}", file=sys.stderr)
100
+ print(json.dumps({
101
+ "hookSpecificOutput": {
102
+ "additionalContext": f"[Plan Review Error] Failed to import required module: {e}. The plan review hook could not load its dependencies.",
103
+ }
104
+ }, ensure_ascii=True))
91
105
  sys.exit(0) # Non-blocking failure
92
106
 
93
107
  # Add scripts directory to path for aggregate_agents import
@@ -99,10 +113,21 @@ try:
99
113
  from aggregate_agents import aggregate_agents
100
114
  except ImportError:
101
115
  def aggregate_agents(agents_dir: Path) -> List[Dict[str, Any]]:
102
- eprint("[cc-native-plan-review] Warning: aggregate_agents not found")
116
+ log_warn("cc-native-plan-review", "aggregate_agents not found")
103
117
  return []
104
118
 
105
119
 
120
+ def skip_with_info(reason: str) -> int:
121
+ """Exit hook with informational additionalContext instead of silently.
122
+
123
+ This ensures Claude always sees WHY the plan review was skipped,
124
+ making failures diagnosable instead of invisible.
125
+ """
126
+ log_info("cc-native-plan-review", f"Skipping: {reason}")
127
+ emit_context(f"[Plan Review Skipped] {reason}", ensure_ascii=True)
128
+ return 0
129
+
130
+
106
131
  # ---------------------------
107
132
  # Default Configuration
108
133
  # ---------------------------
@@ -124,7 +149,7 @@ DEFAULT_AGENT_MODEL: str = "sonnet"
124
149
 
125
150
  DEFAULT_REVIEW_ITERATIONS: Dict[str, int] = {
126
151
  "simple": 1,
127
- "medium": 1,
152
+ "medium": 2,
128
153
  "high": 2,
129
154
  }
130
155
 
@@ -153,24 +178,26 @@ def get_active_context_for_review(session_id: str, project_root: Path) -> Option
153
178
  # Strategy 1: Find by session_id
154
179
  context = get_context_by_session_id(session_id, project_root)
155
180
  if context:
156
- eprint(f"[cc-native-plan-review] Found context by session_id: {context.id}")
181
+ log_info("cc-native-plan-review", f"Found context by session_id: {context.id}")
157
182
  return context
158
183
 
159
184
  # Strategy 2: Single planning context (only planning mode)
160
- in_flight = get_all_in_flight_contexts(project_root)
161
- planning_contexts = [c for c in in_flight if c.in_flight and c.in_flight.mode == "planning"]
185
+ all_active = get_all_contexts(status="active", project_root=project_root)
186
+ # In the new system, "planning" is runtime-only (not persisted).
187
+ # Since this hook fires during ExitPlanMode, any active non-idle context is a candidate.
188
+ planning_contexts = [c for c in all_active if c.mode in ("active", "has_plan")]
162
189
  if len(planning_contexts) == 1:
163
- eprint(f"[cc-native-plan-review] Found single planning context: {planning_contexts[0].id}")
190
+ log_info("cc-native-plan-review", f"Found single planning context: {planning_contexts[0].id}")
164
191
  return planning_contexts[0]
165
192
 
166
193
  # Multiple or no planning contexts found
167
194
  if len(planning_contexts) > 1:
168
- eprint(f"[cc-native-plan-review] Multiple planning contexts ({len(planning_contexts)}), cannot determine which to use")
169
- elif len(in_flight) > 0:
170
- modes = [c.in_flight.mode if c.in_flight else "none" for c in in_flight]
171
- eprint(f"[cc-native-plan-review] Found {len(in_flight)} in-flight context(s) with modes {modes}, but none in 'planning' mode")
195
+ log_warn("cc-native-plan-review", f"Multiple planning contexts ({len(planning_contexts)}), cannot determine which to use")
196
+ elif len(all_active) > 0:
197
+ modes = [c.mode for c in all_active]
198
+ log_info("cc-native-plan-review", f"Found {len(all_active)} active context(s) with modes {modes}, but none in 'planning' mode")
172
199
  else:
173
- eprint("[cc-native-plan-review] No in-flight contexts found")
200
+ log_info("cc-native-plan-review", "No active contexts found")
174
201
  return None
175
202
 
176
203
 
@@ -190,7 +217,7 @@ def load_iteration_state(reviews_dir: Path) -> Optional[Dict[str, Any]]:
190
217
  try:
191
218
  return json.loads(iteration_file.read_text(encoding="utf-8"))
192
219
  except Exception as e:
193
- eprint(f"[cc-native-plan-review] Failed to load iteration state: {e}")
220
+ log_error("cc-native-plan-review", f"Failed to load iteration state: {e}")
194
221
  return None
195
222
 
196
223
 
@@ -211,7 +238,7 @@ def save_iteration_state(reviews_dir: Path, state: Dict[str, Any]) -> bool:
211
238
  iteration_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
212
239
  return True
213
240
  except Exception as e:
214
- eprint(f"[cc-native-plan-review] Failed to save iteration state: {e}")
241
+ log_error("cc-native-plan-review", f"Failed to save iteration state: {e}")
215
242
  return False
216
243
 
217
244
 
@@ -277,14 +304,14 @@ def update_iteration_state_in_context(
277
304
 
278
305
  def should_continue_iterating_context(
279
306
  iteration: Dict[str, Any],
280
- verdict: str,
307
+ review_score: float,
281
308
  config: Optional[Dict[str, Any]] = None,
282
309
  ) -> bool:
283
310
  """Determine if more review iterations are needed.
284
311
 
285
312
  Args:
286
313
  iteration: The iteration state dict
287
- verdict: Current review verdict
314
+ review_score: Score from compute_review_decision (0.0 = all pass, >0 = concerns)
288
315
  config: Optional config dict with earlyExitOnAllPass setting
289
316
 
290
317
  Returns:
@@ -295,19 +322,19 @@ def should_continue_iterating_context(
295
322
 
296
323
  # At or past max iterations - no more iterations
297
324
  if current >= max_iter:
298
- eprint(f"[cc-native-plan-review] At max iterations ({current}/{max_iter}), no more iterations")
325
+ log_info("cc-native-plan-review", f"At max iterations ({current}/{max_iter}), no more iterations")
299
326
  return False
300
327
 
301
328
  # Check early exit on all pass
302
- early_exit = True
329
+ early_exit = False
303
330
  if config:
304
- early_exit = config.get("earlyExitOnAllPass", True)
305
- if early_exit and verdict == "pass":
306
- eprint(f"[cc-native-plan-review] All reviewers passed and earlyExitOnAllPass=true, exiting early")
331
+ early_exit = config.get("earlyExitOnAllPass", False)
332
+ if early_exit and review_score == 0.0:
333
+ log_info("cc-native-plan-review", "All reviewers passed (score=0.0) and earlyExitOnAllPass=true, exiting early")
307
334
  return False
308
335
 
309
- # More iterations available and verdict is not pass (or early exit disabled)
310
- eprint(f"[cc-native-plan-review] Continuing to next iteration ({current + 1}/{max_iter}), verdict={verdict}")
336
+ # More iterations available and score is not zero (or early exit disabled)
337
+ log_info("cc-native-plan-review", f"Continuing to next iteration ({current + 1}/{max_iter}), score={review_score:.2f}")
311
338
  return True
312
339
 
313
340
 
@@ -324,14 +351,13 @@ def load_settings(proj_dir: Path) -> Dict[str, Any]:
324
351
  "codex": {"enabled": True, "model": "", "timeout": 120},
325
352
  "gemini": {"enabled": False, "model": "", "timeout": 120},
326
353
  },
327
- "blockOnFail": False,
328
354
  "display": DEFAULT_DISPLAY.copy(),
329
355
  },
330
356
  "agentReview": {
331
357
  "enabled": True,
332
358
  "orchestrator": DEFAULT_ORCHESTRATOR.copy(),
333
359
  "timeout": 180,
334
- "blockOnFail": True,
360
+ "warnThreshold": 0.5,
335
361
  "legacyMode": False,
336
362
  "display": DEFAULT_DISPLAY.copy(),
337
363
  "agentSelection": DEFAULT_AGENT_SELECTION.copy(),
@@ -375,7 +401,7 @@ def load_settings(proj_dir: Path) -> Dict[str, Any]:
375
401
 
376
402
  # Merge reviewIterations settings
377
403
  merged_agent["reviewIterations"] = {**DEFAULT_REVIEW_ITERATIONS, **agent_review.get("reviewIterations", {})}
378
- merged_agent["earlyExitOnAllPass"] = agent_review.get("earlyExitOnAllPass", True)
404
+ merged_agent["earlyExitOnAllPass"] = agent_review.get("earlyExitOnAllPass", False)
379
405
 
380
406
  return {"planReview": merged_plan, "agentReview": merged_agent}
381
407
 
@@ -394,7 +420,7 @@ def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None
394
420
  default_model = settings.get("agentDefaults", {}).get("model", DEFAULT_AGENT_MODEL)
395
421
 
396
422
  if not agents_data:
397
- eprint("[cc-native-plan-review] No agents found in frontmatter, using defaults")
423
+ log_info("cc-native-plan-review", "No agents found in frontmatter, using defaults")
398
424
  return [
399
425
  AgentConfig(
400
426
  name=a["name"],
@@ -428,25 +454,24 @@ def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None
428
454
  # ---------------------------
429
455
 
430
456
  def main() -> int:
431
- eprint("[cc-native-plan-review] Unified hook started (PreToolUse)")
457
+ log_info("cc-native-plan-review", "Unified hook started (PreToolUse)")
432
458
 
433
459
  # Skip if internal subprocess call (orchestrator, agents)
434
460
  if is_internal_call():
435
- eprint("[cc-native-plan-review] Skipping: internal subprocess call")
461
+ log_debug("cc-native-plan-review", "Skipping: internal subprocess call")
436
462
  return 0
437
463
 
438
464
  try:
439
465
  payload = json.load(sys.stdin)
440
466
  except json.JSONDecodeError as e:
441
- eprint(f"[cc-native-plan-review] Invalid JSON input: {e}")
442
- return 0
467
+ return skip_with_info(f"Invalid JSON input from Claude Code: {e}")
443
468
 
444
469
  tool_name = payload.get("tool_name")
445
- eprint(f"[cc-native-plan-review] tool_name: {tool_name}")
470
+ log_debug("cc-native-plan-review", f"tool_name: {tool_name}")
446
471
 
447
472
  # Only process ExitPlanMode
448
473
  if tool_name != "ExitPlanMode":
449
- eprint("[cc-native-plan-review] Skipping: not ExitPlanMode")
474
+ log_debug("cc-native-plan-review", "Skipping: not ExitPlanMode")
450
475
  return 0
451
476
 
452
477
  session_id = str(payload.get("session_id", "unknown"))
@@ -460,58 +485,57 @@ def main() -> int:
460
485
  agent_review_enabled = agent_settings.get("enabled", True)
461
486
 
462
487
  if not plan_review_enabled and not agent_review_enabled:
463
- eprint("[cc-native-plan-review] Skipping: both plan and agent review disabled")
488
+ log_info("cc-native-plan-review", "Skipping: both plan and agent review disabled")
464
489
  return 0
465
490
 
466
491
  # Find and read plan FIRST (state file is keyed by plan path)
467
492
  plan_path = find_plan_file()
468
493
  if not plan_path:
469
- eprint("[cc-native-plan-review] Skipping: no plan file found in ~/.claude/plans/")
470
- return 0
494
+ return skip_with_info("No plan file found in ~/.claude/plans/. The plan may not have been written yet.")
471
495
 
472
496
  try:
473
497
  plan = Path(plan_path).read_text(encoding="utf-8").strip()
474
498
  except Exception as e:
475
- eprint(f"[cc-native-plan-review] Failed to read plan file: {e}")
476
- return 0
499
+ return skip_with_info(f"Failed to read plan file: {e}")
477
500
 
478
501
  if not plan:
479
- eprint("[cc-native-plan-review] Skipping: plan file is empty")
480
- return 0
502
+ return skip_with_info("Plan file exists but is empty.")
481
503
 
482
- eprint(f"[cc-native-plan-review] Found plan at: {plan_path}")
483
- eprint(f"[cc-native-plan-review] Plan length: {len(plan)} chars")
504
+ log_info("cc-native-plan-review", f"Found plan at: {plan_path}")
505
+ log_debug("cc-native-plan-review", f"Plan length: {len(plan)} chars")
506
+ log_diagnostic("cc-native-plan-review", "receive", f"plan_size={len(plan)}, session={session_id[:8]}",
507
+ inputs={"plan_hash": compute_plan_hash(plan), "plan_size": len(plan),
508
+ "session_id": session_id[:12]})
484
509
 
485
510
  # Find active context for this review (required)
486
511
  active_context = get_active_context_for_review(session_id, base)
487
512
 
488
513
  if not active_context:
489
- eprint("[cc-native-plan-review] Skipping: no active context found")
490
- return 0
514
+ return skip_with_info("No active planning context found for this session. The context system may not have a context in 'planning' mode.")
491
515
 
492
516
  # Get base reviews dir from shared lib, then add cc-native namespace
493
517
  reviews_dir = get_context_reviews_dir(active_context.id, base) / "cc-native"
494
- eprint(f"[cc-native-plan-review] Using context reviews dir: {reviews_dir}")
518
+ log_debug("cc-native-plan-review", f"Using context reviews dir: {reviews_dir}")
495
519
 
496
520
  # Get context path for debug logging
497
521
  context_path = get_context_dir(active_context.id, base)
498
- eprint(f"[cc-native-plan-review] Context path for debug: {context_path}")
499
-
500
- # Check if we've exhausted review iterations from context
501
- existing_iteration = load_iteration_state(reviews_dir)
502
- if existing_iteration:
503
- current = existing_iteration.get("current", 1)
504
- max_iter = existing_iteration.get("max", 1)
505
- if current > max_iter:
506
- eprint(f"[cc-native-plan-review] Skipping: review iterations exhausted ({current}/{max_iter})")
507
- return 0
522
+ log_debug("cc-native-plan-review", f"Context path for debug: {context_path}")
508
523
 
509
- # Plan-hash deduplication
524
+ # Plan-hash deduplication (decision-aware)
510
525
  plan_hash = compute_plan_hash(plan)
511
- eprint(f"[cc-native-plan-review] Plan hash: {plan_hash}")
526
+ log_debug("cc-native-plan-review", f"Plan hash: {plan_hash}")
512
527
  if is_plan_already_reviewed(session_id, plan_hash):
513
- eprint("[cc-native-plan-review] Skipping: plan already reviewed (hash match)")
514
- return 0
528
+ if was_plan_previously_denied(session_id, plan_hash):
529
+ # Plan was denied and hasn't changed — block, don't re-review
530
+ emit_context_and_block(
531
+ "[Plan Review] Plan content unchanged since last review which found issues.",
532
+ "Plan unchanged since denial. Modify the plan to address review findings, "
533
+ "then attempt ExitPlanMode again.",
534
+ )
535
+ return 0
536
+ else:
537
+ # Plan was reviewed and allowed — skip review, allow through
538
+ return skip_with_info("Plan already reviewed and approved (same hash).")
515
539
 
516
540
  # Initialize combined result
517
541
  cli_results: Dict[str, ReviewerResult] = {}
@@ -543,10 +567,16 @@ def main() -> int:
543
567
  timeout=orch_settings.get("timeout", 30),
544
568
  )
545
569
 
546
- eprint(f"[cc-native-plan-review] Codex enabled: {codex_enabled}, Gemini enabled: {gemini_enabled}")
547
- eprint(f"[cc-native-plan-review] Agent library: {[a.name for a in agent_library]}")
548
- eprint(f"[cc-native-plan-review] Enabled agents: {[a.name for a in enabled_agents]}")
549
- eprint(f"[cc-native-plan-review] Orchestrator enabled: {orchestrator_config.enabled}")
570
+ # Compute mandatory agent names early so orchestrator can exclude them
571
+ mandatory_names = set(agent_settings.get("mandatoryAgents", [
572
+ "handoff-readiness", "clarity-auditor", "skeptic"
573
+ ]))
574
+
575
+ log_debug("cc-native-plan-review", f"Codex enabled: {codex_enabled}, Gemini enabled: {gemini_enabled}")
576
+ log_debug("cc-native-plan-review", f"Agent library: {[a.name for a in agent_library]}")
577
+ log_debug("cc-native-plan-review", f"Enabled agents: {[a.name for a in enabled_agents]}")
578
+ log_debug("cc-native-plan-review", f"Mandatory agents: {sorted(mandatory_names)}")
579
+ log_debug("cc-native-plan-review", f"Orchestrator enabled: {orchestrator_config.enabled}")
550
580
 
551
581
  # Run CLI reviewers + orchestrator in parallel
552
582
  phase1_tasks = []
@@ -555,9 +585,9 @@ def main() -> int:
555
585
  if gemini_enabled:
556
586
  phase1_tasks.append(("gemini", lambda: run_gemini_review(plan, REVIEW_SCHEMA, plan_settings)))
557
587
  if orchestrator_config.enabled and enabled_agents and not legacy_mode:
558
- phase1_tasks.append(("orchestrator", lambda: run_orchestrator(plan, enabled_agents, orchestrator_config, agent_settings)))
588
+ phase1_tasks.append(("orchestrator", lambda: run_orchestrator(plan, enabled_agents, orchestrator_config, agent_settings, mandatory_names=mandatory_names)))
559
589
 
560
- eprint(f"[cc-native-plan-review] === PHASE 1: Running {len(phase1_tasks)} tasks in parallel ===")
590
+ log_info("cc-native-plan-review", f"=== PHASE 1: Running {len(phase1_tasks)} tasks in parallel ===")
561
591
 
562
592
  phase1_results: Dict[str, Any] = {}
563
593
  if phase1_tasks:
@@ -567,9 +597,9 @@ def main() -> int:
567
597
  name = futures[future]
568
598
  try:
569
599
  phase1_results[name] = future.result()
570
- eprint(f"[cc-native-plan-review] {name} completed")
600
+ log_info("cc-native-plan-review", f"{name} completed")
571
601
  except Exception as ex:
572
- eprint(f"[cc-native-plan-review] {name} failed: {ex}")
602
+ log_error("cc-native-plan-review", f"{name} failed: {ex}")
573
603
  phase1_results[name] = None
574
604
 
575
605
  # Collect CLI results
@@ -590,14 +620,11 @@ def main() -> int:
590
620
  # PHASE 2: Agent Selection (from orchestrator result)
591
621
  # ============================================
592
622
  if agent_review_enabled:
593
- eprint("[cc-native-plan-review] === PHASE 2: Agent Selection ===")
623
+ log_info("cc-native-plan-review", "=== PHASE 2: Agent Selection ===")
594
624
 
595
625
  selected_agents: List[AgentConfig] = []
596
626
 
597
- # Load mandatory and fallback config
598
- mandatory_names = set(agent_settings.get("mandatoryAgents", [
599
- "handoff-readiness", "clarity-auditor", "skeptic"
600
- ]))
627
+ # Load fallback config (mandatory_names already computed above)
601
628
  fallback_by_complexity = agent_settings.get("fallbackByComplexity", {
602
629
  "simple": 0, "medium": 5, "high": 9
603
630
  })
@@ -607,8 +634,8 @@ def main() -> int:
607
634
  mandatory_agents = [a for a in enabled_agents if a.name in mandatory_names]
608
635
  non_mandatory = [a for a in enabled_agents if a.name not in mandatory_names]
609
636
 
610
- eprint(f"[cc-native-plan-review] Mandatory agents: {[a.name for a in mandatory_agents]}")
611
- eprint(f"[cc-native-plan-review] Non-mandatory pool: {len(non_mandatory)} agents")
637
+ log_debug("cc-native-plan-review", f"Mandatory agents: {[a.name for a in mandatory_agents]}")
638
+ log_debug("cc-native-plan-review", f"Non-mandatory pool: {len(non_mandatory)} agents")
612
639
 
613
640
  if orch_result and not legacy_mode:
614
641
  detected_complexity = orch_result.complexity
@@ -617,35 +644,50 @@ def main() -> int:
617
644
  orch_selected_names = set(orch_result.selected_agents) - mandatory_names
618
645
  orch_selected = [a for a in non_mandatory if a.name in orch_selected_names]
619
646
 
620
- eprint(f"[cc-native-plan-review] Orchestrator selected (non-mandatory): {[a.name for a in orch_selected]}")
647
+ log_debug("cc-native-plan-review", f"Orchestrator selected (non-mandatory): {[a.name for a in orch_selected]}")
648
+
649
+ # Diagnostic: warn if orchestrator returned names not in our agent pool
650
+ unmatched = orch_selected_names - {a.name for a in non_mandatory}
651
+ if unmatched:
652
+ log_warn("cc-native-plan-review", f"Orchestrator selected unknown agents: {unmatched}")
621
653
 
622
- # Random fallback if orchestrator selected zero additional agents
623
- if not orch_selected and non_mandatory:
624
- fallback_count = fallback_by_complexity.get(detected_complexity, 5)
625
- fallback_count = min(fallback_count, len(non_mandatory))
626
- if fallback_count > 0:
627
- orch_selected = random.sample(non_mandatory, fallback_count)
628
- eprint(f"[cc-native-plan-review] Random fallback ({detected_complexity}): {[a.name for a in orch_selected]}")
654
+ # Enforce minimum agent count — top up with random agents if orchestrator selected too few
655
+ min_additional = fallback_by_complexity.get(detected_complexity, 5)
656
+ if len(orch_selected) < min_additional and non_mandatory:
657
+ remaining = [a for a in non_mandatory if a not in orch_selected]
658
+ top_up_count = min(min_additional - len(orch_selected), len(remaining))
659
+ if top_up_count > 0:
660
+ top_up = random.sample(remaining, top_up_count)
661
+ orch_selected.extend(top_up)
662
+ 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]}")
629
663
 
630
664
  # Combine: mandatory + orchestrator/fallback selection
631
665
  selected_agents = mandatory_agents + orch_selected
632
- eprint(f"[cc-native-plan-review] Final selection: {len(selected_agents)} agents ({len(mandatory_agents)} mandatory + {len(orch_selected)} additional)")
666
+ log_info("cc-native-plan-review", f"Final selection: {len(selected_agents)} agents ({len(mandatory_agents)} mandatory + {len(orch_selected)} additional)")
633
667
  else:
634
- eprint("[cc-native-plan-review] Running in legacy mode (all enabled agents)")
668
+ log_info("cc-native-plan-review", "Running in legacy mode (all enabled agents)")
635
669
  selected_agents = enabled_agents
636
670
  detected_complexity = "medium" # Default for legacy mode
637
671
 
672
+ log_diagnostic("cc-native-plan-review", "decide",
673
+ f"Selected {len(selected_agents)} agents, complexity={detected_complexity}",
674
+ decision="agents_selected",
675
+ reasoning=f"orchestrator={orch_result is not None}, legacy={legacy_mode}",
676
+ inputs={"agents": [a.name for a in selected_agents],
677
+ "complexity": detected_complexity,
678
+ "mandatory_count": len([a for a in selected_agents if a.name in mandatory_names])})
679
+
638
680
  # Initialize iteration state based on complexity (after orchestrator runs)
639
681
  if reviews_dir:
640
682
  iteration_state = get_iteration_state_from_context(reviews_dir, detected_complexity, agent_settings)
641
- eprint(f"[cc-native-plan-review] Iteration state: {iteration_state['current']}/{iteration_state['max']} ({detected_complexity})")
683
+ log_debug("cc-native-plan-review", f"Iteration state: {iteration_state['current']}/{iteration_state['max']} ({detected_complexity})")
642
684
 
643
685
  # PHASE 3: Run selected agents in parallel
644
686
  if selected_agents:
645
- eprint("[cc-native-plan-review] === PHASE 3: Agent Reviews ===")
687
+ log_info("cc-native-plan-review", "=== PHASE 3: Agent Reviews ===")
646
688
  max_parallel = agent_settings.get("maxParallelAgents", 0) # 0 = unlimited
647
689
  num_workers = len(selected_agents) if max_parallel <= 0 else min(max_parallel, len(selected_agents))
648
- eprint(f"[cc-native-plan-review] Launching {len(selected_agents)} agents in parallel (workers={num_workers})")
690
+ log_info("cc-native-plan-review", f"Launching {len(selected_agents)} agents in parallel (workers={num_workers})")
649
691
 
650
692
  # Debug log the agent review start
651
693
  debug_log(context_path, session_id, "hook", "agent_review_start", {
@@ -666,9 +708,9 @@ def main() -> int:
666
708
  agent_results[agent.name] = result
667
709
  if result.verdict and result.verdict not in ("skip", "error"):
668
710
  all_verdicts.append(result.verdict)
669
- eprint(f"[cc-native-plan-review] {agent.name} completed with verdict: {result.verdict}")
711
+ log_info("cc-native-plan-review", f"{agent.name} completed with verdict: {result.verdict}")
670
712
  except Exception as ex:
671
- eprint(f"[cc-native-plan-review] {agent.name} failed with exception: {ex}")
713
+ log_error("cc-native-plan-review", f"{agent.name} failed with exception: {ex}")
672
714
  agent_results[agent.name] = ReviewerResult(
673
715
  name=agent.name,
674
716
  ok=False,
@@ -681,11 +723,10 @@ def main() -> int:
681
723
  # ============================================
682
724
  # PHASE 4: Generate Combined Output
683
725
  # ============================================
684
- eprint("[cc-native-plan-review] === PHASE 4: Generate Output ===")
726
+ log_info("cc-native-plan-review", "=== PHASE 4: Generate Output ===")
685
727
 
686
728
  if not cli_results and not agent_results:
687
- eprint("[cc-native-plan-review] No review results, exiting")
688
- return 0
729
+ return skip_with_info("All reviewers failed to produce results. Check stderr logs for details.")
689
730
 
690
731
  overall = worst_verdict(all_verdicts) if all_verdicts else "pass"
691
732
 
@@ -710,37 +751,47 @@ def main() -> int:
710
751
  # Create review folder with datetime and iteration in name
711
752
  review_folder = get_review_folder_path(active_context.id, current_iteration, base)
712
753
  review_folder.mkdir(parents=True, exist_ok=True)
713
- eprint(f"[cc-native-plan-review] Created review folder: {review_folder}")
754
+ log_info("cc-native-plan-review", f"Created review folder: {review_folder}")
714
755
 
715
756
  review_file = write_combined_artifacts(
716
757
  base, plan, combined_result, payload, combined_settings,
717
758
  review_folder=review_folder,
718
759
  iteration=current_iteration,
719
760
  )
720
- eprint(f"[cc-native-plan-review] Saved review: {review_file}")
761
+ log_info("cc-native-plan-review", f"Saved review: {review_file}")
721
762
 
722
- # Build context message
723
- md_content = format_combined_markdown(combined_result, combined_settings)
763
+ # Build inline review summary for additionalContext
764
+ inline_summary = build_inline_review_summary(combined_result)
724
765
 
725
- context_parts = [
726
- "**CC-Native Plan Review Complete**\n\n",
727
- f"Review saved to: `{review_file}`\n\n",
728
- ]
766
+ context_parts = [inline_summary, f"\nFull review: `{review_file}`\n"]
729
767
 
730
- if cli_results:
731
- cli_verdicts = [f"{name}={r.verdict}" for name, r in cli_results.items()]
732
- context_parts.append(f"**CLI Reviewers:** {', '.join(cli_verdicts)}\n")
768
+ # Review decision — only fail triggers a block
769
+ warn_threshold = agent_settings.get("warnThreshold", 0.5)
770
+ should_deny, deny_reason, review_score = compute_review_decision(all_verdicts, warn_threshold)
733
771
 
734
- if orch_result:
735
- context_parts.append(f"**Orchestration:** Complexity=`{orch_result.complexity}`, Category=`{orch_result.category}`, Agents selected: {len(agent_results)}\n")
736
-
737
- context_parts.append("\nUse these findings before starting implementation.\n\n")
738
- context_parts.append(md_content)
772
+ # Count high-severity issues for logging
773
+ high_count = sum(
774
+ 1 for r in list(combined_result.cli_reviewers.values()) + list(combined_result.agents.values())
775
+ if r.data
776
+ for issue in r.data.get("issues", [])
777
+ if issue.get("severity") == "high"
778
+ )
739
779
 
740
- # Check blocking conditions
741
- block_on_fail_plan = plan_settings.get("blockOnFail", False)
742
- block_on_fail_agent = agent_settings.get("blockOnFail", True)
743
- should_block = (overall == "fail") and (block_on_fail_plan or block_on_fail_agent)
780
+ # Structured log entries for review influence tracking
781
+ log_info("cc-native-plan-review", f"REVIEW_DECISION: verdict={combined_result.overall_verdict}, deny={should_deny}, score={review_score:.2f}, high_issues={high_count}")
782
+ log_diagnostic("cc-native-plan-review", "result",
783
+ f"verdict={combined_result.overall_verdict}, deny={should_deny}, high={high_count}",
784
+ decision="deny" if should_deny else "allow",
785
+ reasoning=f"score={review_score:.2f}, threshold={warn_threshold}",
786
+ inputs={"overall_verdict": combined_result.overall_verdict,
787
+ "high_issue_count": high_count, "review_score": round(review_score, 2),
788
+ "cli_count": len(cli_results), "agent_count": len(agent_results)})
789
+
790
+ # Terminal progress indicator
791
+ verdict_emoji = "✅" if not should_deny else "❌"
792
+ eprint(f"[plan-review] {verdict_emoji} {combined_result.overall_verdict.upper()} (score={review_score:.2f})")
793
+ if should_deny:
794
+ eprint(f"[plan-review] Blocking ExitPlanMode — {high_count} high-severity issue(s) found")
744
795
 
745
796
  # Handle iteration logic
746
797
  needs_more_iterations = False
@@ -749,7 +800,7 @@ def main() -> int:
749
800
  iteration_state = update_iteration_state_in_context(reviews_dir, iteration_state, plan_hash, overall)
750
801
 
751
802
  # Check if more iterations needed
752
- if should_continue_iterating_context(iteration_state, overall, agent_settings):
803
+ if should_continue_iterating_context(iteration_state, review_score, agent_settings):
753
804
  needs_more_iterations = True
754
805
  # Increment iteration counter for next round
755
806
  iteration_state["current"] = iteration_state.get("current", 1) + 1
@@ -760,60 +811,59 @@ def main() -> int:
760
811
  iteration_state["current"] = iteration_state.get("current", 1) + 1
761
812
  # Also increment max by 1 to allow another review cycle if the user rejects
762
813
  # the plan and requests changes. Without this, once iterations are exhausted,
763
- # the hook would skip review entirely (line ~498) even if the user sent the
814
+ # the hook would skip review entirely even if the user sent the
764
815
  # planner back to revise. This ensures rejected plans can always be re-reviewed.
765
816
  iteration_state["max"] = iteration_state.get("max", 1) + 1
766
817
  save_iteration_state(reviews_dir, iteration_state)
767
818
 
768
- # Build output with correct Claude Code hook format
769
- # See: https://docs.anthropic.com/en/docs/claude-code/hooks
770
- out: Dict[str, Any] = {
771
- "hookSpecificOutput": {
772
- "hookEventName": "PreToolUse",
773
- "additionalContext": "".join(context_parts),
774
- }
775
- }
819
+ # Emit output with correct Claude Code hook format
820
+ context_text = "".join(context_parts)
821
+
822
+ log_debug("cc-native-plan-review", f"REVIEW_CONTEXT_INJECTED: chars={len(context_text)}, inline_chars={len(inline_summary)}")
823
+
824
+ _REVIEWER_CAVEAT = (
825
+ "Reviewers have limited context compared to your full session — "
826
+ "adopt valid points, use your judgment where they lack context."
827
+ )
828
+
829
+ _RESUBMIT_INSTRUCTION = (
830
+ "IMPORTANT: After revising the plan file, you MUST call ExitPlanMode again "
831
+ "to trigger re-review. Do not end your turn or ask the user without calling ExitPlanMode."
832
+ )
776
833
 
777
- # Handle blocking scenarios - use permissionDecision/permissionDecisionReason inside hookSpecificOutput
778
- # Note: md_content is already in additionalContext, so permissionDecisionReason only needs the instruction
779
834
  if needs_more_iterations:
835
+ mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="deny")
780
836
  current = iteration_state["current"] - 1 # Display the just-completed iteration
781
837
  max_iter = iteration_state["max"]
782
838
  remaining = max_iter - current
783
-
784
- out["hookSpecificOutput"]["permissionDecision"] = "deny"
785
- out["hookSpecificOutput"]["permissionDecisionReason"] = (
786
- f"CC-Native plan review iteration {current}/{max_iter} verdict = {overall.upper()}. "
787
- f"REVISION REQUIRED: Address the issues in additionalContext. "
788
- f"Revise the plan in place, then attempt ExitPlanMode again. "
789
- f"({remaining} revision{'s' if remaining != 1 else ''} remaining)"
839
+ top_issues_text = extract_top_issues_text(combined_result, max_count=3, severity="high")
840
+ emit_context_and_block(
841
+ context_text,
842
+ f"Plan review iteration {current}/{max_iter} FAILED ({deny_reason}, score={review_score:.2f}). "
843
+ f"Critical issues: {top_issues_text}. "
844
+ f"{_REVIEWER_CAVEAT} "
845
+ f"Revise the plan, then call ExitPlanMode again. "
846
+ f"({remaining} revision{'s' if remaining != 1 else ''} remaining) "
847
+ f"{_RESUBMIT_INSTRUCTION}",
790
848
  )
791
- elif should_block:
792
- out["hookSpecificOutput"]["permissionDecision"] = "deny"
793
- out["hookSpecificOutput"]["permissionDecisionReason"] = (
794
- "CC-Native plan review verdict = FAIL. Do NOT start implementation yet. "
795
- "Revise the plan to address the issues in additionalContext, "
796
- "then attempt ExitPlanMode again."
849
+ elif should_deny:
850
+ mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="deny")
851
+ top_issues_text = extract_top_issues_text(combined_result, max_count=3, severity="high")
852
+ emit_context_and_block(
853
+ context_text,
854
+ f"Plan review FAILED ({deny_reason}, score={review_score:.2f}). "
855
+ f"Critical issues: {top_issues_text}. "
856
+ f"{_REVIEWER_CAVEAT} "
857
+ f"Revise the plan, then call ExitPlanMode again. "
858
+ f"{_RESUBMIT_INSTRUCTION}",
797
859
  )
860
+ else:
861
+ mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="allow")
862
+ emit_context(context_text, ensure_ascii=True)
798
863
 
799
- mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state)
800
- # Use ensure_ascii=True to avoid Windows cp1252 encoding errors
801
- print(json.dumps(out, ensure_ascii=True))
802
864
  return 0
803
865
 
804
866
 
805
867
  if __name__ == "__main__":
806
- try:
807
- raise SystemExit(main())
808
- except Exception as e:
809
- import traceback
810
- print(f"[cc-native-plan-review] FATAL ERROR: {e}", file=sys.stderr)
811
- traceback.print_exc(file=sys.stderr)
812
- # Output error to Claude via hook format so it's visible
813
- print(json.dumps({
814
- "hookSpecificOutput": {
815
- "hookEventName": "PreToolUse",
816
- "additionalContext": f"**CC-Native Plan Review Hook Error**\n\nThe hook encountered an error:\n```\n{traceback.format_exc()}\n```\n\nPlease report this issue.",
817
- }
818
- }))
819
- raise SystemExit(1)
868
+ from base.hook_utils import run_hook
869
+ run_hook(main, "cc_native_plan_review")