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