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.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +49 -18
- package/dist/templates/_shared/.claude/settings.json +4 -0
- package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
- package/dist/templates/_shared/hooks/archive_plan.py +87 -178
- package/dist/templates/_shared/hooks/context_monitor.py +128 -194
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +104 -0
- package/dist/templates/_shared/hooks/session_end.py +154 -0
- package/dist/templates/_shared/hooks/session_start.py +145 -59
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
- package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
- package/dist/templates/_shared/lib/base/__init__.py +16 -0
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/constants.py +18 -4
- package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
- package/dist/templates/_shared/lib/base/inference.py +121 -0
- package/dist/templates/_shared/lib/base/logger.py +291 -0
- package/dist/templates/_shared/lib/base/utils.py +49 -11
- package/dist/templates/_shared/lib/context/__init__.py +72 -80
- package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
- package/dist/templates/_shared/lib/context/context_selector.py +491 -0
- package/dist/templates/_shared/lib/context/context_store.py +636 -0
- package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
- package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
- package/dist/templates/_shared/lib/templates/README.md +5 -13
- package/dist/templates/_shared/lib/templates/__init__.py +2 -6
- package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
- package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
- package/dist/templates/_shared/scripts/save_handoff.py +39 -19
- package/dist/templates/_shared/scripts/status_line.py +701 -0
- package/dist/templates/_shared/workflows/handoff.md +9 -3
- package/dist/templates/cc-native/.claude/settings.json +64 -9
- package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
- package/dist/templates/cc-native/MIGRATION.md +1 -1
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
- package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
- package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
- package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
- package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
- package/dist/templates/_shared/lib/context/cache.py +0 -444
- package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
- package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
- package/dist/templates/_shared/lib/context/discovery.py +0 -444
- package/dist/templates/_shared/lib/context/event_log.py +0 -308
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -290
- package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
elif len(
|
|
170
|
-
modes = [c.
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
329
|
+
early_exit = False
|
|
303
330
|
if config:
|
|
304
|
-
early_exit = config.get("earlyExitOnAllPass",
|
|
305
|
-
if early_exit and
|
|
306
|
-
|
|
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
|
|
310
|
-
|
|
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
|
-
"
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
476
|
-
return 0
|
|
499
|
+
return skip_with_info(f"Failed to read plan file: {e}")
|
|
477
500
|
|
|
478
501
|
if not plan:
|
|
479
|
-
|
|
480
|
-
return 0
|
|
502
|
+
return skip_with_info("Plan file exists but is empty.")
|
|
481
503
|
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
+
log_debug("cc-native-plan-review", f"Plan hash: {plan_hash}")
|
|
512
527
|
if is_plan_already_reviewed(session_id, plan_hash):
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
+
log_info("cc-native-plan-review", f"{name} completed")
|
|
571
601
|
except Exception as ex:
|
|
572
|
-
|
|
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
|
-
|
|
623
|
+
log_info("cc-native-plan-review", "=== PHASE 2: Agent Selection ===")
|
|
594
624
|
|
|
595
625
|
selected_agents: List[AgentConfig] = []
|
|
596
626
|
|
|
597
|
-
# Load
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
+
log_info("cc-native-plan-review", f"{agent.name} completed with verdict: {result.verdict}")
|
|
670
712
|
except Exception as ex:
|
|
671
|
-
|
|
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
|
-
|
|
726
|
+
log_info("cc-native-plan-review", "=== PHASE 4: Generate Output ===")
|
|
685
727
|
|
|
686
728
|
if not cli_results and not agent_results:
|
|
687
|
-
|
|
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
|
-
|
|
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
|
-
|
|
761
|
+
log_info("cc-native-plan-review", f"Saved review: {review_file}")
|
|
721
762
|
|
|
722
|
-
# Build
|
|
723
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
#
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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,
|
|
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
|
|
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
|
-
#
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
f"
|
|
787
|
-
f"
|
|
788
|
-
f"
|
|
789
|
-
f"
|
|
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
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
"
|
|
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
|
-
|
|
807
|
-
|
|
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")
|