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.
- package/bin/run.js +5 -2
- package/dist/lib/claude-settings-types.d.ts +2 -0
- package/dist/templates/CLAUDE.md +3 -3
- 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_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 +104 -247
- package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
- package/dist/templates/_shared/hooks/pre_compact.py +47 -32
- package/dist/templates/_shared/hooks/session_end.py +103 -60
- package/dist/templates/_shared/hooks/session_start.py +110 -81
- package/dist/templates/_shared/hooks/task_create_capture.py +26 -50
- package/dist/templates/_shared/hooks/task_update_capture.py +42 -115
- package/dist/templates/_shared/hooks/user_prompt_submit.py +61 -61
- 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__/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/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 +42 -9
- 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 +1 -38
- 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 +41 -8
- 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/hooks/CLAUDE.md +49 -21
- 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 -55
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +163 -131
- 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 +6 -4
- 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 +34 -29
- 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 +207 -40
- 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 -177
- package/dist/templates/_shared/lib/context/auto_state.py +0 -167
- 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 -1057
- package/dist/templates/_shared/lib/context/discovery.py +0 -554
- package/dist/templates/_shared/lib/context/event_log.py +0 -316
- package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
- package/dist/templates/_shared/lib/context/task_sync.py +0 -407
- 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,18 @@ 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)
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
|
|
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
|
|
330
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
+
log_debug("cc-native-plan-review", f"Plan hash: {plan_hash}")
|
|
518
527
|
if is_plan_already_reviewed(session_id, plan_hash):
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
+
log_info("cc-native-plan-review", f"{name} completed")
|
|
588
601
|
except Exception as ex:
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
+
log_info("cc-native-plan-review", f"{agent.name} completed with verdict: {result.verdict}")
|
|
691
712
|
except Exception as ex:
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
755
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
#
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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,
|
|
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
|
|
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
|
-
#
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
f"
|
|
806
|
-
f"
|
|
807
|
-
f"
|
|
808
|
-
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}",
|
|
809
848
|
)
|
|
810
|
-
elif
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
"
|
|
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
|
-
|
|
826
|
-
|
|
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")
|