aiwcli 0.9.8 → 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 (116) 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 +3 -3
  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_capture.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  17. package/dist/templates/_shared/hooks/context_monitor.py +104 -247
  18. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  19. package/dist/templates/_shared/hooks/pre_compact.py +47 -32
  20. package/dist/templates/_shared/hooks/session_end.py +103 -60
  21. package/dist/templates/_shared/hooks/session_start.py +110 -81
  22. package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
  23. package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
  24. package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
  25. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  26. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  32. package/dist/templates/_shared/lib/base/inference.py +121 -0
  33. package/dist/templates/_shared/lib/base/logger.py +291 -0
  34. package/dist/templates/_shared/lib/base/utils.py +42 -9
  35. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  36. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  38. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  39. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  45. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  46. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  47. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  48. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  49. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  50. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  51. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  52. package/dist/templates/_shared/lib/templates/README.md +5 -13
  53. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  54. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  56. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  57. package/dist/templates/_shared/lib/templates/plan_context.py +1 -38
  58. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  61. package/dist/templates/_shared/scripts/status_line.py +701 -0
  62. package/dist/templates/_shared/workflows/handoff.md +9 -3
  63. package/dist/templates/cc-native/.claude/settings.json +41 -8
  64. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  65. package/dist/templates/cc-native/MIGRATION.md +1 -1
  66. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  67. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +49 -21
  68. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  69. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  70. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  71. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -55
  75. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
  76. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  79. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +6 -4
  80. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  81. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  82. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  83. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  87. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +34 -29
  88. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  91. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  96. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  97. package/dist/templates/cc-native/_cc-native/lib/utils.py +207 -40
  98. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  99. package/oclif.manifest.json +1 -1
  100. package/package.json +1 -1
  101. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  102. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -177
  103. package/dist/templates/_shared/lib/context/auto_state.py +0 -167
  104. package/dist/templates/_shared/lib/context/cache.py +0 -444
  105. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  106. package/dist/templates/_shared/lib/context/context_manager.py +0 -1057
  107. package/dist/templates/_shared/lib/context/discovery.py +0 -554
  108. package/dist/templates/_shared/lib/context/event_log.py +0 -316
  109. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  110. package/dist/templates/_shared/lib/context/task_sync.py +0 -407
  111. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  112. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  113. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  114. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  115. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  116. 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,18 @@ 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)
91
100
  print(json.dumps({
92
101
  "hookSpecificOutput": {
93
102
  "additionalContext": f"[Plan Review Error] Failed to import required module: {e}. The plan review hook could not load its dependencies.",
@@ -104,7 +113,7 @@ try:
104
113
  from aggregate_agents import aggregate_agents
105
114
  except ImportError:
106
115
  def aggregate_agents(agents_dir: Path) -> List[Dict[str, Any]]:
107
- eprint("[cc-native-plan-review] Warning: aggregate_agents not found")
116
+ log_warn("cc-native-plan-review", "aggregate_agents not found")
108
117
  return []
109
118
 
110
119
 
@@ -114,12 +123,8 @@ def skip_with_info(reason: str) -> int:
114
123
  This ensures Claude always sees WHY the plan review was skipped,
115
124
  making failures diagnosable instead of invisible.
116
125
  """
117
- eprint(f"[cc-native-plan-review] Skipping: {reason}")
118
- print(json.dumps({
119
- "hookSpecificOutput": {
120
- "additionalContext": f"[Plan Review Skipped] {reason}",
121
- }
122
- }, ensure_ascii=True))
126
+ log_info("cc-native-plan-review", f"Skipping: {reason}")
127
+ emit_context(f"[Plan Review Skipped] {reason}", ensure_ascii=True)
123
128
  return 0
124
129
 
125
130
 
@@ -173,24 +178,26 @@ def get_active_context_for_review(session_id: str, project_root: Path) -> Option
173
178
  # Strategy 1: Find by session_id
174
179
  context = get_context_by_session_id(session_id, project_root)
175
180
  if context:
176
- 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}")
177
182
  return context
178
183
 
179
184
  # Strategy 2: Single planning context (only planning mode)
180
185
  all_active = get_all_contexts(status="active", project_root=project_root)
181
- planning_contexts = [c for c in all_active if c.in_flight and c.in_flight.mode == "planning"]
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")]
182
189
  if len(planning_contexts) == 1:
183
- 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}")
184
191
  return planning_contexts[0]
185
192
 
186
193
  # Multiple or no planning contexts found
187
194
  if len(planning_contexts) > 1:
188
- eprint(f"[cc-native-plan-review] Multiple planning contexts ({len(planning_contexts)}), cannot determine which to use")
195
+ log_warn("cc-native-plan-review", f"Multiple planning contexts ({len(planning_contexts)}), cannot determine which to use")
189
196
  elif len(all_active) > 0:
190
- modes = [c.in_flight.mode if c.in_flight else "none" for c in all_active]
191
- eprint(f"[cc-native-plan-review] Found {len(all_active)} active context(s) with modes {modes}, but none in 'planning' mode")
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")
192
199
  else:
193
- eprint("[cc-native-plan-review] No active contexts found")
200
+ log_info("cc-native-plan-review", "No active contexts found")
194
201
  return None
195
202
 
196
203
 
@@ -210,7 +217,7 @@ def load_iteration_state(reviews_dir: Path) -> Optional[Dict[str, Any]]:
210
217
  try:
211
218
  return json.loads(iteration_file.read_text(encoding="utf-8"))
212
219
  except Exception as e:
213
- 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}")
214
221
  return None
215
222
 
216
223
 
@@ -231,7 +238,7 @@ def save_iteration_state(reviews_dir: Path, state: Dict[str, Any]) -> bool:
231
238
  iteration_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
232
239
  return True
233
240
  except Exception as e:
234
- 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}")
235
242
  return False
236
243
 
237
244
 
@@ -297,14 +304,14 @@ def update_iteration_state_in_context(
297
304
 
298
305
  def should_continue_iterating_context(
299
306
  iteration: Dict[str, Any],
300
- verdict: str,
307
+ review_score: float,
301
308
  config: Optional[Dict[str, Any]] = None,
302
309
  ) -> bool:
303
310
  """Determine if more review iterations are needed.
304
311
 
305
312
  Args:
306
313
  iteration: The iteration state dict
307
- verdict: Current review verdict
314
+ review_score: Score from compute_review_decision (0.0 = all pass, >0 = concerns)
308
315
  config: Optional config dict with earlyExitOnAllPass setting
309
316
 
310
317
  Returns:
@@ -315,19 +322,19 @@ def should_continue_iterating_context(
315
322
 
316
323
  # At or past max iterations - no more iterations
317
324
  if current >= max_iter:
318
- 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")
319
326
  return False
320
327
 
321
328
  # Check early exit on all pass
322
329
  early_exit = False
323
330
  if config:
324
331
  early_exit = config.get("earlyExitOnAllPass", False)
325
- if early_exit and verdict == "pass":
326
- eprint(f"[cc-native-plan-review] All reviewers passed and earlyExitOnAllPass=true, exiting early")
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")
327
334
  return False
328
335
 
329
- # More iterations available and verdict is not pass (or early exit disabled)
330
- 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}")
331
338
  return True
332
339
 
333
340
 
@@ -344,14 +351,13 @@ def load_settings(proj_dir: Path) -> Dict[str, Any]:
344
351
  "codex": {"enabled": True, "model": "", "timeout": 120},
345
352
  "gemini": {"enabled": False, "model": "", "timeout": 120},
346
353
  },
347
- "blockOnFail": False,
348
354
  "display": DEFAULT_DISPLAY.copy(),
349
355
  },
350
356
  "agentReview": {
351
357
  "enabled": True,
352
358
  "orchestrator": DEFAULT_ORCHESTRATOR.copy(),
353
359
  "timeout": 180,
354
- "blockOnFail": True,
360
+ "warnThreshold": 0.5,
355
361
  "legacyMode": False,
356
362
  "display": DEFAULT_DISPLAY.copy(),
357
363
  "agentSelection": DEFAULT_AGENT_SELECTION.copy(),
@@ -414,7 +420,7 @@ def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None
414
420
  default_model = settings.get("agentDefaults", {}).get("model", DEFAULT_AGENT_MODEL)
415
421
 
416
422
  if not agents_data:
417
- 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")
418
424
  return [
419
425
  AgentConfig(
420
426
  name=a["name"],
@@ -448,11 +454,11 @@ def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None
448
454
  # ---------------------------
449
455
 
450
456
  def main() -> int:
451
- eprint("[cc-native-plan-review] Unified hook started (PreToolUse)")
457
+ log_info("cc-native-plan-review", "Unified hook started (PreToolUse)")
452
458
 
453
459
  # Skip if internal subprocess call (orchestrator, agents)
454
460
  if is_internal_call():
455
- eprint("[cc-native-plan-review] Skipping: internal subprocess call")
461
+ log_debug("cc-native-plan-review", "Skipping: internal subprocess call")
456
462
  return 0
457
463
 
458
464
  try:
@@ -461,11 +467,11 @@ def main() -> int:
461
467
  return skip_with_info(f"Invalid JSON input from Claude Code: {e}")
462
468
 
463
469
  tool_name = payload.get("tool_name")
464
- eprint(f"[cc-native-plan-review] tool_name: {tool_name}")
470
+ log_debug("cc-native-plan-review", f"tool_name: {tool_name}")
465
471
 
466
472
  # Only process ExitPlanMode
467
473
  if tool_name != "ExitPlanMode":
468
- eprint("[cc-native-plan-review] Skipping: not ExitPlanMode")
474
+ log_debug("cc-native-plan-review", "Skipping: not ExitPlanMode")
469
475
  return 0
470
476
 
471
477
  session_id = str(payload.get("session_id", "unknown"))
@@ -479,7 +485,7 @@ def main() -> int:
479
485
  agent_review_enabled = agent_settings.get("enabled", True)
480
486
 
481
487
  if not plan_review_enabled and not agent_review_enabled:
482
- 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")
483
489
  return 0
484
490
 
485
491
  # Find and read plan FIRST (state file is keyed by plan path)
@@ -495,8 +501,11 @@ def main() -> int:
495
501
  if not plan:
496
502
  return skip_with_info("Plan file exists but is empty.")
497
503
 
498
- eprint(f"[cc-native-plan-review] Found plan at: {plan_path}")
499
- 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]})
500
509
 
501
510
  # Find active context for this review (required)
502
511
  active_context = get_active_context_for_review(session_id, base)
@@ -506,23 +515,27 @@ def main() -> int:
506
515
 
507
516
  # Get base reviews dir from shared lib, then add cc-native namespace
508
517
  reviews_dir = get_context_reviews_dir(active_context.id, base) / "cc-native"
509
- 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}")
510
519
 
511
520
  # Get context path for debug logging
512
521
  context_path = get_context_dir(active_context.id, base)
513
- eprint(f"[cc-native-plan-review] Context path for debug: {context_path}")
522
+ log_debug("cc-native-plan-review", f"Context path for debug: {context_path}")
514
523
 
515
- # Plan-hash deduplication
524
+ # Plan-hash deduplication (decision-aware)
516
525
  plan_hash = compute_plan_hash(plan)
517
- eprint(f"[cc-native-plan-review] Plan hash: {plan_hash}")
526
+ log_debug("cc-native-plan-review", f"Plan hash: {plan_hash}")
518
527
  if is_plan_already_reviewed(session_id, plan_hash):
519
- # Still increment iteration even when skipping review
520
- existing_iteration = load_iteration_state(reviews_dir)
521
- if existing_iteration:
522
- existing_iteration["current"] = existing_iteration.get("current", 1) + 1
523
- save_iteration_state(reviews_dir, existing_iteration)
524
- eprint(f"[cc-native-plan-review] Incremented iteration to {existing_iteration['current']} (review skipped, same hash)")
525
- return skip_with_info("Plan content unchanged since last review (same hash). Modify the plan to trigger a new review.")
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).")
526
539
 
527
540
  # Initialize combined result
528
541
  cli_results: Dict[str, ReviewerResult] = {}
@@ -559,11 +572,11 @@ def main() -> int:
559
572
  "handoff-readiness", "clarity-auditor", "skeptic"
560
573
  ]))
561
574
 
562
- eprint(f"[cc-native-plan-review] Codex enabled: {codex_enabled}, Gemini enabled: {gemini_enabled}")
563
- eprint(f"[cc-native-plan-review] Agent library: {[a.name for a in agent_library]}")
564
- eprint(f"[cc-native-plan-review] Enabled agents: {[a.name for a in enabled_agents]}")
565
- eprint(f"[cc-native-plan-review] Mandatory agents: {sorted(mandatory_names)}")
566
- eprint(f"[cc-native-plan-review] Orchestrator enabled: {orchestrator_config.enabled}")
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}")
567
580
 
568
581
  # Run CLI reviewers + orchestrator in parallel
569
582
  phase1_tasks = []
@@ -574,7 +587,7 @@ def main() -> int:
574
587
  if orchestrator_config.enabled and enabled_agents and not legacy_mode:
575
588
  phase1_tasks.append(("orchestrator", lambda: run_orchestrator(plan, enabled_agents, orchestrator_config, agent_settings, mandatory_names=mandatory_names)))
576
589
 
577
- 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 ===")
578
591
 
579
592
  phase1_results: Dict[str, Any] = {}
580
593
  if phase1_tasks:
@@ -584,9 +597,9 @@ def main() -> int:
584
597
  name = futures[future]
585
598
  try:
586
599
  phase1_results[name] = future.result()
587
- eprint(f"[cc-native-plan-review] {name} completed")
600
+ log_info("cc-native-plan-review", f"{name} completed")
588
601
  except Exception as ex:
589
- eprint(f"[cc-native-plan-review] {name} failed: {ex}")
602
+ log_error("cc-native-plan-review", f"{name} failed: {ex}")
590
603
  phase1_results[name] = None
591
604
 
592
605
  # Collect CLI results
@@ -607,7 +620,7 @@ def main() -> int:
607
620
  # PHASE 2: Agent Selection (from orchestrator result)
608
621
  # ============================================
609
622
  if agent_review_enabled:
610
- eprint("[cc-native-plan-review] === PHASE 2: Agent Selection ===")
623
+ log_info("cc-native-plan-review", "=== PHASE 2: Agent Selection ===")
611
624
 
612
625
  selected_agents: List[AgentConfig] = []
613
626
 
@@ -621,8 +634,8 @@ def main() -> int:
621
634
  mandatory_agents = [a for a in enabled_agents if a.name in mandatory_names]
622
635
  non_mandatory = [a for a in enabled_agents if a.name not in mandatory_names]
623
636
 
624
- eprint(f"[cc-native-plan-review] Mandatory agents: {[a.name for a in mandatory_agents]}")
625
- 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")
626
639
 
627
640
  if orch_result and not legacy_mode:
628
641
  detected_complexity = orch_result.complexity
@@ -631,12 +644,12 @@ def main() -> int:
631
644
  orch_selected_names = set(orch_result.selected_agents) - mandatory_names
632
645
  orch_selected = [a for a in non_mandatory if a.name in orch_selected_names]
633
646
 
634
- 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]}")
635
648
 
636
649
  # Diagnostic: warn if orchestrator returned names not in our agent pool
637
650
  unmatched = orch_selected_names - {a.name for a in non_mandatory}
638
651
  if unmatched:
639
- eprint(f"[cc-native-plan-review] WARNING: Orchestrator selected unknown agents: {unmatched}")
652
+ log_warn("cc-native-plan-review", f"Orchestrator selected unknown agents: {unmatched}")
640
653
 
641
654
  # Enforce minimum agent count — top up with random agents if orchestrator selected too few
642
655
  min_additional = fallback_by_complexity.get(detected_complexity, 5)
@@ -646,27 +659,35 @@ def main() -> int:
646
659
  if top_up_count > 0:
647
660
  top_up = random.sample(remaining, top_up_count)
648
661
  orch_selected.extend(top_up)
649
- eprint(f"[cc-native-plan-review] Topped up {top_up_count} agents to meet {detected_complexity} minimum: {[a.name for a in 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]}")
650
663
 
651
664
  # Combine: mandatory + orchestrator/fallback selection
652
665
  selected_agents = mandatory_agents + orch_selected
653
- 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)")
654
667
  else:
655
- 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)")
656
669
  selected_agents = enabled_agents
657
670
  detected_complexity = "medium" # Default for legacy mode
658
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
+
659
680
  # Initialize iteration state based on complexity (after orchestrator runs)
660
681
  if reviews_dir:
661
682
  iteration_state = get_iteration_state_from_context(reviews_dir, detected_complexity, agent_settings)
662
- 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})")
663
684
 
664
685
  # PHASE 3: Run selected agents in parallel
665
686
  if selected_agents:
666
- eprint("[cc-native-plan-review] === PHASE 3: Agent Reviews ===")
687
+ log_info("cc-native-plan-review", "=== PHASE 3: Agent Reviews ===")
667
688
  max_parallel = agent_settings.get("maxParallelAgents", 0) # 0 = unlimited
668
689
  num_workers = len(selected_agents) if max_parallel <= 0 else min(max_parallel, len(selected_agents))
669
- 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})")
670
691
 
671
692
  # Debug log the agent review start
672
693
  debug_log(context_path, session_id, "hook", "agent_review_start", {
@@ -687,9 +708,9 @@ def main() -> int:
687
708
  agent_results[agent.name] = result
688
709
  if result.verdict and result.verdict not in ("skip", "error"):
689
710
  all_verdicts.append(result.verdict)
690
- 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}")
691
712
  except Exception as ex:
692
- 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}")
693
714
  agent_results[agent.name] = ReviewerResult(
694
715
  name=agent.name,
695
716
  ok=False,
@@ -702,7 +723,7 @@ def main() -> int:
702
723
  # ============================================
703
724
  # PHASE 4: Generate Combined Output
704
725
  # ============================================
705
- eprint("[cc-native-plan-review] === PHASE 4: Generate Output ===")
726
+ log_info("cc-native-plan-review", "=== PHASE 4: Generate Output ===")
706
727
 
707
728
  if not cli_results and not agent_results:
708
729
  return skip_with_info("All reviewers failed to produce results. Check stderr logs for details.")
@@ -730,37 +751,47 @@ def main() -> int:
730
751
  # Create review folder with datetime and iteration in name
731
752
  review_folder = get_review_folder_path(active_context.id, current_iteration, base)
732
753
  review_folder.mkdir(parents=True, exist_ok=True)
733
- eprint(f"[cc-native-plan-review] Created review folder: {review_folder}")
754
+ log_info("cc-native-plan-review", f"Created review folder: {review_folder}")
734
755
 
735
756
  review_file = write_combined_artifacts(
736
757
  base, plan, combined_result, payload, combined_settings,
737
758
  review_folder=review_folder,
738
759
  iteration=current_iteration,
739
760
  )
740
- eprint(f"[cc-native-plan-review] Saved review: {review_file}")
741
-
742
- # Build context message
743
- md_content = format_combined_markdown(combined_result, combined_settings)
761
+ log_info("cc-native-plan-review", f"Saved review: {review_file}")
744
762
 
745
- context_parts = [
746
- "**CC-Native Plan Review Complete**\n\n",
747
- f"Review saved to: `{review_file}`\n\n",
748
- ]
763
+ # Build inline review summary for additionalContext
764
+ inline_summary = build_inline_review_summary(combined_result)
749
765
 
750
- if cli_results:
751
- cli_verdicts = [f"{name}={r.verdict}" for name, r in cli_results.items()]
752
- context_parts.append(f"**CLI Reviewers:** {', '.join(cli_verdicts)}\n")
766
+ context_parts = [inline_summary, f"\nFull review: `{review_file}`\n"]
753
767
 
754
- if orch_result:
755
- context_parts.append(f"**Orchestration:** Complexity=`{orch_result.complexity}`, Category=`{orch_result.category}`, Agents selected: {len(agent_results)}\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)
756
771
 
757
- context_parts.append("\nUse these findings before starting implementation.\n\n")
758
- 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
+ )
759
779
 
760
- # Check blocking conditions
761
- block_on_fail_plan = plan_settings.get("blockOnFail", False)
762
- block_on_fail_agent = agent_settings.get("blockOnFail", True)
763
- 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")
764
795
 
765
796
  # Handle iteration logic
766
797
  needs_more_iterations = False
@@ -769,7 +800,7 @@ def main() -> int:
769
800
  iteration_state = update_iteration_state_in_context(reviews_dir, iteration_state, plan_hash, overall)
770
801
 
771
802
  # Check if more iterations needed
772
- if should_continue_iterating_context(iteration_state, overall, agent_settings):
803
+ if should_continue_iterating_context(iteration_state, review_score, agent_settings):
773
804
  needs_more_iterations = True
774
805
  # Increment iteration counter for next round
775
806
  iteration_state["current"] = iteration_state.get("current", 1) + 1
@@ -780,58 +811,59 @@ def main() -> int:
780
811
  iteration_state["current"] = iteration_state.get("current", 1) + 1
781
812
  # Also increment max by 1 to allow another review cycle if the user rejects
782
813
  # the plan and requests changes. Without this, once iterations are exhausted,
783
- # 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
784
815
  # planner back to revise. This ensures rejected plans can always be re-reviewed.
785
816
  iteration_state["max"] = iteration_state.get("max", 1) + 1
786
817
  save_iteration_state(reviews_dir, iteration_state)
787
818
 
788
- # Build output with correct Claude Code hook format
789
- # See: https://docs.anthropic.com/en/docs/claude-code/hooks
790
- out: Dict[str, Any] = {
791
- "hookSpecificOutput": {
792
- "additionalContext": "".join(context_parts),
793
- }
794
- }
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
+ )
795
833
 
796
- # Handle blocking scenarios - use permissionDecision/permissionDecisionReason inside hookSpecificOutput
797
- # Note: md_content is already in additionalContext, so permissionDecisionReason only needs the instruction
798
834
  if needs_more_iterations:
835
+ mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="deny")
799
836
  current = iteration_state["current"] - 1 # Display the just-completed iteration
800
837
  max_iter = iteration_state["max"]
801
838
  remaining = max_iter - current
802
-
803
- out["hookSpecificOutput"]["permissionDecision"] = "deny"
804
- out["hookSpecificOutput"]["permissionDecisionReason"] = (
805
- f"CC-Native plan review iteration {current}/{max_iter} verdict = {overall.upper()}. "
806
- f"REVISION REQUIRED: Address the issues in additionalContext. "
807
- f"Revise the plan in place, then attempt ExitPlanMode again. "
808
- 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}",
809
848
  )
810
- elif should_block:
811
- out["hookSpecificOutput"]["permissionDecision"] = "deny"
812
- out["hookSpecificOutput"]["permissionDecisionReason"] = (
813
- "CC-Native plan review verdict = FAIL. Do NOT start implementation yet. "
814
- "Revise the plan to address the issues in additionalContext, "
815
- "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}",
816
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)
817
863
 
818
- mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state)
819
- # Use ensure_ascii=True to avoid Windows cp1252 encoding errors
820
- print(json.dumps(out, ensure_ascii=True))
821
864
  return 0
822
865
 
823
866
 
824
867
  if __name__ == "__main__":
825
- try:
826
- raise SystemExit(main())
827
- except Exception as e:
828
- import traceback
829
- print(f"[cc-native-plan-review] FATAL ERROR: {e}", file=sys.stderr)
830
- traceback.print_exc(file=sys.stderr)
831
- # Output error to Claude via hook format so it's visible
832
- print(json.dumps({
833
- "hookSpecificOutput": {
834
- "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.",
835
- }
836
- }))
837
- raise SystemExit(1)
868
+ from base.hook_utils import run_hook
869
+ run_hook(main, "cc_native_plan_review")