aiwcli 0.10.2 → 0.11.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 +1 -1
- package/dist/commands/clear.d.ts +11 -6
- package/dist/commands/clear.js +229 -381
- package/dist/commands/init/index.d.ts +1 -17
- package/dist/commands/init/index.js +22 -107
- package/dist/lib/gitignore-manager.d.ts +32 -0
- package/dist/lib/gitignore-manager.js +141 -2
- package/dist/lib/template-installer.d.ts +7 -12
- package/dist/lib/template-installer.js +69 -193
- package/dist/lib/template-settings-reconstructor.d.ts +35 -0
- package/dist/lib/template-settings-reconstructor.js +130 -0
- package/dist/templates/CLAUDE.md +8 -8
- package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
- package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
- package/dist/templates/_shared/.claude/settings.json +7 -7
- package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
- package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
- package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
- package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
- package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
- package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
- package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
- package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
- package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
- package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
- package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
- package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
- package/dist/templates/_shared/lib-ts/base/constants.ts +306 -0
- package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
- package/dist/templates/_shared/lib-ts/base/hook-utils.ts +439 -0
- package/dist/templates/_shared/lib-ts/base/inference.ts +252 -0
- package/dist/templates/_shared/lib-ts/base/logger.ts +250 -0
- package/dist/templates/_shared/lib-ts/base/state-io.ts +116 -0
- package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
- package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +162 -0
- package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +438 -0
- package/dist/templates/_shared/lib-ts/context/context-selector.ts +515 -0
- package/dist/templates/_shared/lib-ts/context/context-store.ts +707 -0
- package/dist/templates/_shared/lib-ts/context/plan-manager.ts +316 -0
- package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -0
- package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +216 -0
- package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
- package/dist/templates/_shared/lib-ts/package.json +21 -0
- package/dist/templates/_shared/lib-ts/templates/formatters.ts +104 -0
- package/dist/templates/_shared/{lib/templates/plan_context.py → lib-ts/templates/plan-context.ts} +14 -22
- package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
- package/dist/templates/_shared/lib-ts/types.ts +164 -0
- package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
- package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
- package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
- package/dist/templates/_shared/scripts/status_line.ts +733 -0
- package/dist/templates/cc-native/.claude/settings.json +175 -185
- package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
- package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
- package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -3
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
- package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
- package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
- package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
- package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
- package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
- package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
- package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
- package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
- package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
- package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
- package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
- package/dist/templates/cc-native/_cc-native/plan-review.config.json +12 -16
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/lib/template-merger.d.ts +0 -47
- package/dist/lib/template-merger.js +0 -162
- package/dist/templates/_shared/hooks/__init__.py +0 -16
- 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 +0 -169
- package/dist/templates/_shared/hooks/context_monitor.py +0 -270
- package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
- package/dist/templates/_shared/hooks/pre_compact.py +0 -104
- package/dist/templates/_shared/hooks/session_end.py +0 -173
- package/dist/templates/_shared/hooks/session_start.py +0 -206
- package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
- package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
- package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
- package/dist/templates/_shared/lib/__init__.py +0 -1
- package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__init__.py +0 -65
- package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/atomic_write.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__/stop_words.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
- package/dist/templates/_shared/lib/base/constants.py +0 -358
- package/dist/templates/_shared/lib/base/hook_utils.py +0 -341
- package/dist/templates/_shared/lib/base/inference.py +0 -318
- package/dist/templates/_shared/lib/base/logger.py +0 -291
- package/dist/templates/_shared/lib/base/stop_words.py +0 -213
- package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
- package/dist/templates/_shared/lib/base/utils.py +0 -242
- package/dist/templates/_shared/lib/context/__init__.py +0 -102
- 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_extractor.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__/event_log.cpython-313.pyc +0 -0
- package/dist/templates/_shared/lib/context/__pycache__/plan_archive.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_sync.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 +0 -317
- package/dist/templates/_shared/lib/context/context_selector.py +0 -508
- package/dist/templates/_shared/lib/context/context_store.py +0 -653
- package/dist/templates/_shared/lib/context/plan_manager.py +0 -204
- package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
- package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
- 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 +0 -278
- package/dist/templates/_shared/lib/templates/README.md +0 -206
- package/dist/templates/_shared/lib/templates/__init__.py +0 -36
- 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__/persona_questions.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/formatters.py +0 -146
- 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 +0 -357
- package/dist/templates/_shared/scripts/status_line.py +0 -701
- package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
- package/dist/templates/cc-native/MIGRATION.md +0 -86
- package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
- package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
- package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
- package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
- package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
- package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
- package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
- package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
- package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
- package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
- package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
- package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
- package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
- 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 +0 -130
- package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -869
- package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
- package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
- package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
- package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.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/constants.py +0 -45
- package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
- package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
- package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
- 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 +0 -215
- package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
- package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
- package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
- package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
- package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1027
- 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 +0 -168
- package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
|
@@ -1,869 +0,0 @@
|
|
|
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 os
|
|
32
|
-
import random
|
|
33
|
-
import sys
|
|
34
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
35
|
-
from datetime import datetime
|
|
36
|
-
from pathlib import Path
|
|
37
|
-
from typing import Any, Dict, List, Optional
|
|
38
|
-
|
|
39
|
-
# Import shared library
|
|
40
|
-
try:
|
|
41
|
-
_lib = Path(__file__).parent.parent / "lib"
|
|
42
|
-
sys.path.insert(0, str(_lib))
|
|
43
|
-
|
|
44
|
-
# Add shared library path
|
|
45
|
-
_shared = Path(__file__).parent.parent.parent / "_shared"
|
|
46
|
-
sys.path.insert(0, str(_shared))
|
|
47
|
-
|
|
48
|
-
# Import subprocess and hook utilities
|
|
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
|
|
52
|
-
|
|
53
|
-
from utils import (
|
|
54
|
-
DEFAULT_DISPLAY,
|
|
55
|
-
DEFAULT_SANITIZATION,
|
|
56
|
-
REVIEW_SCHEMA,
|
|
57
|
-
ReviewerResult,
|
|
58
|
-
CombinedReviewResult,
|
|
59
|
-
project_dir,
|
|
60
|
-
eprint,
|
|
61
|
-
find_plan_file,
|
|
62
|
-
compute_plan_hash,
|
|
63
|
-
compute_review_decision,
|
|
64
|
-
is_plan_already_reviewed,
|
|
65
|
-
was_plan_previously_denied,
|
|
66
|
-
mark_plan_reviewed,
|
|
67
|
-
worst_verdict,
|
|
68
|
-
format_combined_markdown,
|
|
69
|
-
write_combined_artifacts,
|
|
70
|
-
build_inline_review_summary,
|
|
71
|
-
extract_top_issues_text,
|
|
72
|
-
load_config,
|
|
73
|
-
get_display_settings,
|
|
74
|
-
)
|
|
75
|
-
from reviewers import (
|
|
76
|
-
run_codex_review,
|
|
77
|
-
run_gemini_review,
|
|
78
|
-
run_agent_review,
|
|
79
|
-
AgentConfig,
|
|
80
|
-
OrchestratorConfig,
|
|
81
|
-
)
|
|
82
|
-
from orchestrator import (
|
|
83
|
-
run_orchestrator,
|
|
84
|
-
DEFAULT_AGENT_SELECTION,
|
|
85
|
-
DEFAULT_COMPLEXITY_CATEGORIES,
|
|
86
|
-
)
|
|
87
|
-
# Import shared context system
|
|
88
|
-
from lib.context.context_store import (
|
|
89
|
-
get_context_by_session_id,
|
|
90
|
-
get_all_contexts,
|
|
91
|
-
)
|
|
92
|
-
from lib.base.constants import get_context_reviews_dir, get_review_folder_path, get_context_dir
|
|
93
|
-
from debug import debug_log, debug_raw
|
|
94
|
-
except ImportError as e:
|
|
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))
|
|
105
|
-
sys.exit(0) # Non-blocking failure
|
|
106
|
-
|
|
107
|
-
# Add scripts directory to path for aggregate_agents import
|
|
108
|
-
_scripts_dir = Path(__file__).parent.parent / "scripts"
|
|
109
|
-
if str(_scripts_dir) not in sys.path:
|
|
110
|
-
sys.path.insert(0, str(_scripts_dir))
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
from aggregate_agents import aggregate_agents
|
|
114
|
-
except ImportError:
|
|
115
|
-
def aggregate_agents(agents_dir: Path) -> List[Dict[str, Any]]:
|
|
116
|
-
log_warn("cc-native-plan-review", "aggregate_agents not found")
|
|
117
|
-
return []
|
|
118
|
-
|
|
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
|
-
|
|
131
|
-
# ---------------------------
|
|
132
|
-
# Default Configuration
|
|
133
|
-
# ---------------------------
|
|
134
|
-
|
|
135
|
-
DEFAULT_AGENTS: List[Dict[str, Any]] = [
|
|
136
|
-
{"name": "architect-reviewer", "model": "sonnet", "focus": "architectural concerns and scalability", "enabled": True, "categories": ["code", "infrastructure", "design"]},
|
|
137
|
-
{"name": "penetration-tester", "model": "sonnet", "focus": "security vulnerabilities and attack vectors", "enabled": True, "categories": ["code", "infrastructure"]},
|
|
138
|
-
{"name": "performance-engineer", "model": "sonnet", "focus": "performance bottlenecks and optimization", "enabled": True, "categories": ["code", "infrastructure"]},
|
|
139
|
-
{"name": "accessibility-tester", "model": "sonnet", "focus": "accessibility compliance and UX concerns", "enabled": True, "categories": ["code", "design"]},
|
|
140
|
-
]
|
|
141
|
-
|
|
142
|
-
DEFAULT_ORCHESTRATOR: Dict[str, Any] = {
|
|
143
|
-
"enabled": True,
|
|
144
|
-
"model": "haiku",
|
|
145
|
-
"timeout": 30,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
DEFAULT_AGENT_MODEL: str = "sonnet"
|
|
149
|
-
|
|
150
|
-
DEFAULT_REVIEW_ITERATIONS: Dict[str, int] = {
|
|
151
|
-
"simple": 1,
|
|
152
|
-
"medium": 2,
|
|
153
|
-
"high": 2,
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# ---------------------------
|
|
158
|
-
# Context-based State Management
|
|
159
|
-
# ---------------------------
|
|
160
|
-
|
|
161
|
-
def get_active_context_for_review(session_id: str, project_root: Path) -> Optional[Any]:
|
|
162
|
-
"""Find active context for plan review.
|
|
163
|
-
|
|
164
|
-
Strategy:
|
|
165
|
-
1. Find context by session_id
|
|
166
|
-
2. Fallback: Single context in 'planning' mode
|
|
167
|
-
3. Return None if multiple planning contexts or no planning contexts found
|
|
168
|
-
|
|
169
|
-
Only triggers for contexts in 'planning' mode, not 'handoff_pending' or other modes.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
session_id: Current session ID
|
|
173
|
-
project_root: Project root path
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
Context object or None
|
|
177
|
-
"""
|
|
178
|
-
# Strategy 1: Find by session_id
|
|
179
|
-
context = get_context_by_session_id(session_id, project_root)
|
|
180
|
-
if context:
|
|
181
|
-
log_info("cc-native-plan-review", f"Found context by session_id: {context.id}")
|
|
182
|
-
return context
|
|
183
|
-
|
|
184
|
-
# Strategy 2: Single planning context (only planning mode)
|
|
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")]
|
|
189
|
-
if len(planning_contexts) == 1:
|
|
190
|
-
log_info("cc-native-plan-review", f"Found single planning context: {planning_contexts[0].id}")
|
|
191
|
-
return planning_contexts[0]
|
|
192
|
-
|
|
193
|
-
# Multiple or no planning contexts found
|
|
194
|
-
if len(planning_contexts) > 1:
|
|
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")
|
|
199
|
-
else:
|
|
200
|
-
log_info("cc-native-plan-review", "No active contexts found")
|
|
201
|
-
return None
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def load_iteration_state(reviews_dir: Path) -> Optional[Dict[str, Any]]:
|
|
205
|
-
"""Load iteration state from context reviews folder.
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
reviews_dir: Path to the reviews directory
|
|
209
|
-
|
|
210
|
-
Returns:
|
|
211
|
-
Iteration state dict or None if not found
|
|
212
|
-
"""
|
|
213
|
-
iteration_file = reviews_dir / "iteration.json"
|
|
214
|
-
if not iteration_file.exists():
|
|
215
|
-
return None
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
return json.loads(iteration_file.read_text(encoding="utf-8"))
|
|
219
|
-
except Exception as e:
|
|
220
|
-
log_error("cc-native-plan-review", f"Failed to load iteration state: {e}")
|
|
221
|
-
return None
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def save_iteration_state(reviews_dir: Path, state: Dict[str, Any]) -> bool:
|
|
225
|
-
"""Save iteration state to context reviews folder.
|
|
226
|
-
|
|
227
|
-
Args:
|
|
228
|
-
reviews_dir: Path to the reviews directory
|
|
229
|
-
state: Iteration state dict
|
|
230
|
-
|
|
231
|
-
Returns:
|
|
232
|
-
True on success, False on failure
|
|
233
|
-
"""
|
|
234
|
-
iteration_file = reviews_dir / "iteration.json"
|
|
235
|
-
try:
|
|
236
|
-
reviews_dir.mkdir(parents=True, exist_ok=True)
|
|
237
|
-
state["schema_version"] = "1.0.0"
|
|
238
|
-
iteration_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
239
|
-
return True
|
|
240
|
-
except Exception as e:
|
|
241
|
-
log_error("cc-native-plan-review", f"Failed to save iteration state: {e}")
|
|
242
|
-
return False
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def get_iteration_state_from_context(
|
|
246
|
-
reviews_dir: Path,
|
|
247
|
-
complexity: str,
|
|
248
|
-
config: Optional[Dict[str, Any]] = None,
|
|
249
|
-
) -> Dict[str, Any]:
|
|
250
|
-
"""Get or initialize iteration state based on complexity.
|
|
251
|
-
|
|
252
|
-
Args:
|
|
253
|
-
reviews_dir: Path to the reviews directory
|
|
254
|
-
complexity: Plan complexity level (simple/medium/high)
|
|
255
|
-
config: Optional config dict with reviewIterations settings
|
|
256
|
-
|
|
257
|
-
Returns:
|
|
258
|
-
Iteration dict with: current, max, complexity, history
|
|
259
|
-
"""
|
|
260
|
-
existing = load_iteration_state(reviews_dir)
|
|
261
|
-
if existing:
|
|
262
|
-
return existing
|
|
263
|
-
|
|
264
|
-
# Initialize new iteration state
|
|
265
|
-
review_iterations = DEFAULT_REVIEW_ITERATIONS.copy()
|
|
266
|
-
if config:
|
|
267
|
-
review_iterations.update(config.get("reviewIterations", {}))
|
|
268
|
-
max_iterations = review_iterations.get(complexity, 1)
|
|
269
|
-
|
|
270
|
-
return {
|
|
271
|
-
"current": 1,
|
|
272
|
-
"max": max_iterations,
|
|
273
|
-
"complexity": complexity,
|
|
274
|
-
"history": [],
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def update_iteration_state_in_context(
|
|
279
|
-
reviews_dir: Path,
|
|
280
|
-
iteration: Dict[str, Any],
|
|
281
|
-
plan_hash: str,
|
|
282
|
-
verdict: str,
|
|
283
|
-
) -> Dict[str, Any]:
|
|
284
|
-
"""Record review result in iteration history.
|
|
285
|
-
|
|
286
|
-
Args:
|
|
287
|
-
reviews_dir: Path to the reviews directory
|
|
288
|
-
iteration: The iteration state dict
|
|
289
|
-
plan_hash: Hash of the current plan content
|
|
290
|
-
verdict: Review verdict (pass/warn/fail)
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
Updated iteration state dict
|
|
294
|
-
"""
|
|
295
|
-
from datetime import datetime
|
|
296
|
-
|
|
297
|
-
iteration["history"].append({
|
|
298
|
-
"hash": plan_hash,
|
|
299
|
-
"verdict": verdict,
|
|
300
|
-
"timestamp": datetime.now().isoformat(),
|
|
301
|
-
})
|
|
302
|
-
return iteration
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def should_continue_iterating_context(
|
|
306
|
-
iteration: Dict[str, Any],
|
|
307
|
-
review_score: float,
|
|
308
|
-
config: Optional[Dict[str, Any]] = None,
|
|
309
|
-
) -> bool:
|
|
310
|
-
"""Determine if more review iterations are needed.
|
|
311
|
-
|
|
312
|
-
Args:
|
|
313
|
-
iteration: The iteration state dict
|
|
314
|
-
review_score: Score from compute_review_decision (0.0 = all pass, >0 = concerns)
|
|
315
|
-
config: Optional config dict with earlyExitOnAllPass setting
|
|
316
|
-
|
|
317
|
-
Returns:
|
|
318
|
-
True if more iterations needed, False otherwise
|
|
319
|
-
"""
|
|
320
|
-
current = iteration.get("current", 1)
|
|
321
|
-
max_iter = iteration.get("max", 1)
|
|
322
|
-
|
|
323
|
-
# At or past max iterations - no more iterations
|
|
324
|
-
if current >= max_iter:
|
|
325
|
-
log_info("cc-native-plan-review", f"At max iterations ({current}/{max_iter}), no more iterations")
|
|
326
|
-
return False
|
|
327
|
-
|
|
328
|
-
# Check early exit on all pass
|
|
329
|
-
early_exit = False
|
|
330
|
-
if config:
|
|
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")
|
|
334
|
-
return False
|
|
335
|
-
|
|
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}")
|
|
338
|
-
return True
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
# ---------------------------
|
|
342
|
-
# Settings Loading
|
|
343
|
-
# ---------------------------
|
|
344
|
-
|
|
345
|
-
def load_settings(proj_dir: Path) -> Dict[str, Any]:
|
|
346
|
-
"""Load CC-Native settings from _cc-native/plan-review.config.json"""
|
|
347
|
-
defaults = {
|
|
348
|
-
"planReview": {
|
|
349
|
-
"enabled": True,
|
|
350
|
-
"reviewers": {
|
|
351
|
-
"codex": {"enabled": True, "model": "", "timeout": 120},
|
|
352
|
-
"gemini": {"enabled": False, "model": "", "timeout": 120},
|
|
353
|
-
},
|
|
354
|
-
"display": DEFAULT_DISPLAY.copy(),
|
|
355
|
-
},
|
|
356
|
-
"agentReview": {
|
|
357
|
-
"enabled": True,
|
|
358
|
-
"orchestrator": DEFAULT_ORCHESTRATOR.copy(),
|
|
359
|
-
"timeout": 180,
|
|
360
|
-
"warnThreshold": 0.5,
|
|
361
|
-
"legacyMode": False,
|
|
362
|
-
"display": DEFAULT_DISPLAY.copy(),
|
|
363
|
-
"agentSelection": DEFAULT_AGENT_SELECTION.copy(),
|
|
364
|
-
"agentDefaults": {"model": DEFAULT_AGENT_MODEL},
|
|
365
|
-
"complexityCategories": DEFAULT_COMPLEXITY_CATEGORIES.copy(),
|
|
366
|
-
"sanitization": DEFAULT_SANITIZATION.copy(),
|
|
367
|
-
},
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
config = load_config(proj_dir)
|
|
371
|
-
if not config:
|
|
372
|
-
return defaults
|
|
373
|
-
|
|
374
|
-
# Merge planReview settings
|
|
375
|
-
plan_review = config.get("planReview", {})
|
|
376
|
-
merged_plan = defaults["planReview"].copy()
|
|
377
|
-
merged_plan.update(plan_review)
|
|
378
|
-
if "reviewers" in plan_review:
|
|
379
|
-
merged_plan["reviewers"] = defaults["planReview"]["reviewers"].copy()
|
|
380
|
-
merged_plan["reviewers"].update(plan_review["reviewers"])
|
|
381
|
-
merged_plan["display"] = get_display_settings(config, "planReview")
|
|
382
|
-
|
|
383
|
-
# Merge agentReview settings
|
|
384
|
-
agent_review = config.get("agentReview", {})
|
|
385
|
-
merged_agent = defaults["agentReview"].copy()
|
|
386
|
-
merged_agent.update(agent_review)
|
|
387
|
-
|
|
388
|
-
# Handle orchestrator nested config
|
|
389
|
-
if "orchestrator" not in merged_agent or not isinstance(merged_agent["orchestrator"], dict):
|
|
390
|
-
merged_agent["orchestrator"] = DEFAULT_ORCHESTRATOR.copy()
|
|
391
|
-
else:
|
|
392
|
-
orch = DEFAULT_ORCHESTRATOR.copy()
|
|
393
|
-
orch.update(merged_agent["orchestrator"])
|
|
394
|
-
merged_agent["orchestrator"] = orch
|
|
395
|
-
|
|
396
|
-
merged_agent["display"] = get_display_settings(config, "agentReview")
|
|
397
|
-
merged_agent["agentSelection"] = {**DEFAULT_AGENT_SELECTION, **config.get("agentSelection", {})}
|
|
398
|
-
merged_agent["agentDefaults"] = {**{"model": DEFAULT_AGENT_MODEL}, **config.get("agentDefaults", {})}
|
|
399
|
-
merged_agent["complexityCategories"] = config.get("complexityCategories", DEFAULT_COMPLEXITY_CATEGORIES.copy())
|
|
400
|
-
merged_agent["sanitization"] = {**DEFAULT_SANITIZATION, **config.get("sanitization", {})}
|
|
401
|
-
|
|
402
|
-
# Merge reviewIterations settings
|
|
403
|
-
merged_agent["reviewIterations"] = {**DEFAULT_REVIEW_ITERATIONS, **agent_review.get("reviewIterations", {})}
|
|
404
|
-
merged_agent["earlyExitOnAllPass"] = agent_review.get("earlyExitOnAllPass", False)
|
|
405
|
-
|
|
406
|
-
return {"planReview": merged_plan, "agentReview": merged_agent}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def load_agent_library(proj_dir: Path, settings: Optional[Dict[str, Any]] = None) -> List[AgentConfig]:
|
|
410
|
-
"""Load agent library by auto-detecting from frontmatter.
|
|
411
|
-
|
|
412
|
-
Agents are loaded from _cc-native/agents/ directory. The markdown body
|
|
413
|
-
of each agent file becomes the system_prompt for --system-prompt invocation.
|
|
414
|
-
"""
|
|
415
|
-
# aggregate_agents now defaults to _cc-native/agents/ relative to the script
|
|
416
|
-
agents_data = aggregate_agents()
|
|
417
|
-
|
|
418
|
-
default_model = DEFAULT_AGENT_MODEL
|
|
419
|
-
if settings:
|
|
420
|
-
default_model = settings.get("agentDefaults", {}).get("model", DEFAULT_AGENT_MODEL)
|
|
421
|
-
|
|
422
|
-
if not agents_data:
|
|
423
|
-
log_info("cc-native-plan-review", "No agents found in frontmatter, using defaults")
|
|
424
|
-
return [
|
|
425
|
-
AgentConfig(
|
|
426
|
-
name=a["name"],
|
|
427
|
-
model=a.get("model", default_model),
|
|
428
|
-
focus=a.get("focus", "general review"),
|
|
429
|
-
enabled=a.get("enabled", True),
|
|
430
|
-
categories=a.get("categories", ["code"]),
|
|
431
|
-
)
|
|
432
|
-
for a in DEFAULT_AGENTS
|
|
433
|
-
]
|
|
434
|
-
|
|
435
|
-
agents = []
|
|
436
|
-
for a in agents_data:
|
|
437
|
-
if a.get("name") == "plan-orchestrator":
|
|
438
|
-
continue
|
|
439
|
-
agents.append(AgentConfig(
|
|
440
|
-
name=a["name"],
|
|
441
|
-
model=a.get("model", default_model),
|
|
442
|
-
focus=a.get("focus", "general review"),
|
|
443
|
-
enabled=a.get("enabled", True),
|
|
444
|
-
categories=a.get("categories", ["code"]),
|
|
445
|
-
description=a.get("description", ""),
|
|
446
|
-
system_prompt=a.get("system_prompt", ""),
|
|
447
|
-
))
|
|
448
|
-
|
|
449
|
-
return agents
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
# ---------------------------
|
|
453
|
-
# Main Hook
|
|
454
|
-
# ---------------------------
|
|
455
|
-
|
|
456
|
-
def main() -> int:
|
|
457
|
-
log_info("cc-native-plan-review", "Unified hook started (PreToolUse)")
|
|
458
|
-
|
|
459
|
-
# Skip if internal subprocess call (orchestrator, agents)
|
|
460
|
-
if is_internal_call():
|
|
461
|
-
log_debug("cc-native-plan-review", "Skipping: internal subprocess call")
|
|
462
|
-
return 0
|
|
463
|
-
|
|
464
|
-
try:
|
|
465
|
-
payload = json.load(sys.stdin)
|
|
466
|
-
except json.JSONDecodeError as e:
|
|
467
|
-
return skip_with_info(f"Invalid JSON input from Claude Code: {e}")
|
|
468
|
-
|
|
469
|
-
tool_name = payload.get("tool_name")
|
|
470
|
-
log_debug("cc-native-plan-review", f"tool_name: {tool_name}")
|
|
471
|
-
|
|
472
|
-
# Only process ExitPlanMode
|
|
473
|
-
if tool_name != "ExitPlanMode":
|
|
474
|
-
log_debug("cc-native-plan-review", "Skipping: not ExitPlanMode")
|
|
475
|
-
return 0
|
|
476
|
-
|
|
477
|
-
session_id = str(payload.get("session_id", "unknown"))
|
|
478
|
-
base = project_dir(payload)
|
|
479
|
-
settings = load_settings(base)
|
|
480
|
-
|
|
481
|
-
plan_settings = settings.get("planReview", {})
|
|
482
|
-
agent_settings = settings.get("agentReview", {})
|
|
483
|
-
|
|
484
|
-
plan_review_enabled = plan_settings.get("enabled", True)
|
|
485
|
-
agent_review_enabled = agent_settings.get("enabled", True)
|
|
486
|
-
|
|
487
|
-
if not plan_review_enabled and not agent_review_enabled:
|
|
488
|
-
log_info("cc-native-plan-review", "Skipping: both plan and agent review disabled")
|
|
489
|
-
return 0
|
|
490
|
-
|
|
491
|
-
# Find and read plan FIRST (state file is keyed by plan path)
|
|
492
|
-
plan_path = find_plan_file()
|
|
493
|
-
if not plan_path:
|
|
494
|
-
return skip_with_info("No plan file found in ~/.claude/plans/. The plan may not have been written yet.")
|
|
495
|
-
|
|
496
|
-
try:
|
|
497
|
-
plan = Path(plan_path).read_text(encoding="utf-8").strip()
|
|
498
|
-
except Exception as e:
|
|
499
|
-
return skip_with_info(f"Failed to read plan file: {e}")
|
|
500
|
-
|
|
501
|
-
if not plan:
|
|
502
|
-
return skip_with_info("Plan file exists but is empty.")
|
|
503
|
-
|
|
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]})
|
|
509
|
-
|
|
510
|
-
# Find active context for this review (required)
|
|
511
|
-
active_context = get_active_context_for_review(session_id, base)
|
|
512
|
-
|
|
513
|
-
if not active_context:
|
|
514
|
-
return skip_with_info("No active planning context found for this session. The context system may not have a context in 'planning' mode.")
|
|
515
|
-
|
|
516
|
-
# Get base reviews dir from shared lib, then add cc-native namespace
|
|
517
|
-
reviews_dir = get_context_reviews_dir(active_context.id, base) / "cc-native"
|
|
518
|
-
log_debug("cc-native-plan-review", f"Using context reviews dir: {reviews_dir}")
|
|
519
|
-
|
|
520
|
-
# Get context path for debug logging
|
|
521
|
-
context_path = get_context_dir(active_context.id, base)
|
|
522
|
-
log_debug("cc-native-plan-review", f"Context path for debug: {context_path}")
|
|
523
|
-
|
|
524
|
-
# Plan-hash deduplication (decision-aware)
|
|
525
|
-
plan_hash = compute_plan_hash(plan)
|
|
526
|
-
log_debug("cc-native-plan-review", f"Plan hash: {plan_hash}")
|
|
527
|
-
if is_plan_already_reviewed(session_id, plan_hash):
|
|
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).")
|
|
539
|
-
|
|
540
|
-
# Initialize combined result
|
|
541
|
-
cli_results: Dict[str, ReviewerResult] = {}
|
|
542
|
-
orch_result = None
|
|
543
|
-
agent_results: Dict[str, ReviewerResult] = {}
|
|
544
|
-
all_verdicts: List[str] = []
|
|
545
|
-
iteration_state: Optional[Dict[str, Any]] = None
|
|
546
|
-
detected_complexity: str = "medium" # Will be updated by orchestrator
|
|
547
|
-
|
|
548
|
-
# ============================================
|
|
549
|
-
# PHASE 1 & 2: CLI Reviewers + Orchestrator (PARALLEL)
|
|
550
|
-
# ============================================
|
|
551
|
-
# Run CLI reviewers and orchestrator concurrently for speed
|
|
552
|
-
reviewers_config = plan_settings.get("reviewers", {}) if plan_review_enabled else {}
|
|
553
|
-
codex_enabled = plan_review_enabled and reviewers_config.get("codex", {}).get("enabled", True)
|
|
554
|
-
gemini_enabled = plan_review_enabled and reviewers_config.get("gemini", {}).get("enabled", False)
|
|
555
|
-
|
|
556
|
-
agent_library = load_agent_library(base, agent_settings) if agent_review_enabled else []
|
|
557
|
-
# Load all agents regardless of enabled status - enabled:false only prevents
|
|
558
|
-
# Claude Code auto-suggestion, not plan-review usage
|
|
559
|
-
enabled_agents = agent_library
|
|
560
|
-
timeout = agent_settings.get("timeout", 120)
|
|
561
|
-
legacy_mode = agent_settings.get("legacyMode", False)
|
|
562
|
-
|
|
563
|
-
orch_settings = agent_settings.get("orchestrator", DEFAULT_ORCHESTRATOR)
|
|
564
|
-
orchestrator_config = OrchestratorConfig(
|
|
565
|
-
enabled=orch_settings.get("enabled", True) and agent_review_enabled,
|
|
566
|
-
model=orch_settings.get("model", "haiku"),
|
|
567
|
-
timeout=orch_settings.get("timeout", 30),
|
|
568
|
-
)
|
|
569
|
-
|
|
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}")
|
|
580
|
-
|
|
581
|
-
# Run CLI reviewers + orchestrator in parallel
|
|
582
|
-
phase1_tasks = []
|
|
583
|
-
if codex_enabled:
|
|
584
|
-
phase1_tasks.append(("codex", lambda: run_codex_review(plan, REVIEW_SCHEMA, plan_settings)))
|
|
585
|
-
if gemini_enabled:
|
|
586
|
-
phase1_tasks.append(("gemini", lambda: run_gemini_review(plan, REVIEW_SCHEMA, plan_settings)))
|
|
587
|
-
if orchestrator_config.enabled and enabled_agents and not legacy_mode:
|
|
588
|
-
phase1_tasks.append(("orchestrator", lambda: run_orchestrator(plan, enabled_agents, orchestrator_config, agent_settings, mandatory_names=mandatory_names)))
|
|
589
|
-
|
|
590
|
-
log_info("cc-native-plan-review", f"=== PHASE 1: Running {len(phase1_tasks)} tasks in parallel ===")
|
|
591
|
-
|
|
592
|
-
phase1_results: Dict[str, Any] = {}
|
|
593
|
-
if phase1_tasks:
|
|
594
|
-
with ThreadPoolExecutor(max_workers=len(phase1_tasks)) as executor:
|
|
595
|
-
futures = {executor.submit(task_fn): name for name, task_fn in phase1_tasks}
|
|
596
|
-
for future in as_completed(futures):
|
|
597
|
-
name = futures[future]
|
|
598
|
-
try:
|
|
599
|
-
phase1_results[name] = future.result()
|
|
600
|
-
log_info("cc-native-plan-review", f"{name} completed")
|
|
601
|
-
except Exception as ex:
|
|
602
|
-
log_error("cc-native-plan-review", f"{name} failed: {ex}")
|
|
603
|
-
phase1_results[name] = None
|
|
604
|
-
|
|
605
|
-
# Collect CLI results
|
|
606
|
-
if "codex" in phase1_results and phase1_results["codex"]:
|
|
607
|
-
cli_results["codex"] = phase1_results["codex"]
|
|
608
|
-
if phase1_results["codex"].verdict and phase1_results["codex"].verdict not in ("skip", "error"):
|
|
609
|
-
all_verdicts.append(phase1_results["codex"].verdict)
|
|
610
|
-
if "gemini" in phase1_results and phase1_results["gemini"]:
|
|
611
|
-
cli_results["gemini"] = phase1_results["gemini"]
|
|
612
|
-
if phase1_results["gemini"].verdict and phase1_results["gemini"].verdict not in ("skip", "error"):
|
|
613
|
-
all_verdicts.append(phase1_results["gemini"].verdict)
|
|
614
|
-
|
|
615
|
-
# Get orchestrator result
|
|
616
|
-
if "orchestrator" in phase1_results and phase1_results["orchestrator"]:
|
|
617
|
-
orch_result = phase1_results["orchestrator"]
|
|
618
|
-
|
|
619
|
-
# ============================================
|
|
620
|
-
# PHASE 2: Agent Selection (from orchestrator result)
|
|
621
|
-
# ============================================
|
|
622
|
-
if agent_review_enabled:
|
|
623
|
-
log_info("cc-native-plan-review", "=== PHASE 2: Agent Selection ===")
|
|
624
|
-
|
|
625
|
-
selected_agents: List[AgentConfig] = []
|
|
626
|
-
|
|
627
|
-
# Load fallback config (mandatory_names already computed above)
|
|
628
|
-
fallback_by_complexity = agent_settings.get("fallbackByComplexity", {
|
|
629
|
-
"simple": 0, "medium": 5, "high": 9
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
if enabled_agents:
|
|
633
|
-
# Split into mandatory and non-mandatory pools
|
|
634
|
-
mandatory_agents = [a for a in enabled_agents if a.name in mandatory_names]
|
|
635
|
-
non_mandatory = [a for a in enabled_agents if a.name not in mandatory_names]
|
|
636
|
-
|
|
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")
|
|
639
|
-
|
|
640
|
-
if orch_result and not legacy_mode:
|
|
641
|
-
detected_complexity = orch_result.complexity
|
|
642
|
-
|
|
643
|
-
# Get orchestrator's additional selections (excluding mandatory since they always run)
|
|
644
|
-
orch_selected_names = set(orch_result.selected_agents) - mandatory_names
|
|
645
|
-
orch_selected = [a for a in non_mandatory if a.name in orch_selected_names]
|
|
646
|
-
|
|
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}")
|
|
653
|
-
|
|
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]}")
|
|
663
|
-
|
|
664
|
-
# Combine: mandatory + orchestrator/fallback selection
|
|
665
|
-
selected_agents = mandatory_agents + orch_selected
|
|
666
|
-
log_info("cc-native-plan-review", f"Final selection: {len(selected_agents)} agents ({len(mandatory_agents)} mandatory + {len(orch_selected)} additional)")
|
|
667
|
-
else:
|
|
668
|
-
log_info("cc-native-plan-review", "Running in legacy mode (all enabled agents)")
|
|
669
|
-
selected_agents = enabled_agents
|
|
670
|
-
detected_complexity = "medium" # Default for legacy mode
|
|
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
|
-
|
|
680
|
-
# Initialize iteration state based on complexity (after orchestrator runs)
|
|
681
|
-
if reviews_dir:
|
|
682
|
-
iteration_state = get_iteration_state_from_context(reviews_dir, detected_complexity, agent_settings)
|
|
683
|
-
log_debug("cc-native-plan-review", f"Iteration state: {iteration_state['current']}/{iteration_state['max']} ({detected_complexity})")
|
|
684
|
-
|
|
685
|
-
# PHASE 3: Run selected agents in parallel
|
|
686
|
-
if selected_agents:
|
|
687
|
-
log_info("cc-native-plan-review", "=== PHASE 3: Agent Reviews ===")
|
|
688
|
-
max_parallel = agent_settings.get("maxParallelAgents", 0) # 0 = unlimited
|
|
689
|
-
num_workers = len(selected_agents) if max_parallel <= 0 else min(max_parallel, len(selected_agents))
|
|
690
|
-
log_info("cc-native-plan-review", f"Launching {len(selected_agents)} agents in parallel (workers={num_workers})")
|
|
691
|
-
|
|
692
|
-
# Debug log the agent review start
|
|
693
|
-
debug_log(context_path, session_id, "hook", "agent_review_start", {
|
|
694
|
-
"agents": [a.name for a in selected_agents],
|
|
695
|
-
"timeout": timeout,
|
|
696
|
-
"complexity": detected_complexity,
|
|
697
|
-
})
|
|
698
|
-
|
|
699
|
-
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
700
|
-
futures = {
|
|
701
|
-
executor.submit(run_agent_review, plan, agent, REVIEW_SCHEMA, timeout, context_path, session_id): agent
|
|
702
|
-
for agent in selected_agents
|
|
703
|
-
}
|
|
704
|
-
for future in as_completed(futures):
|
|
705
|
-
agent = futures[future]
|
|
706
|
-
try:
|
|
707
|
-
result = future.result()
|
|
708
|
-
agent_results[agent.name] = result
|
|
709
|
-
if result.verdict and result.verdict not in ("skip", "error"):
|
|
710
|
-
all_verdicts.append(result.verdict)
|
|
711
|
-
log_info("cc-native-plan-review", f"{agent.name} completed with verdict: {result.verdict}")
|
|
712
|
-
except Exception as ex:
|
|
713
|
-
log_error("cc-native-plan-review", f"{agent.name} failed with exception: {ex}")
|
|
714
|
-
agent_results[agent.name] = ReviewerResult(
|
|
715
|
-
name=agent.name,
|
|
716
|
-
ok=False,
|
|
717
|
-
verdict="error",
|
|
718
|
-
data={},
|
|
719
|
-
raw="",
|
|
720
|
-
err=str(ex),
|
|
721
|
-
)
|
|
722
|
-
|
|
723
|
-
# ============================================
|
|
724
|
-
# PHASE 4: Generate Combined Output
|
|
725
|
-
# ============================================
|
|
726
|
-
log_info("cc-native-plan-review", "=== PHASE 4: Generate Output ===")
|
|
727
|
-
|
|
728
|
-
if not cli_results and not agent_results:
|
|
729
|
-
return skip_with_info("All reviewers failed to produce results. Check stderr logs for details.")
|
|
730
|
-
|
|
731
|
-
overall = worst_verdict(all_verdicts) if all_verdicts else "pass"
|
|
732
|
-
|
|
733
|
-
combined_result = CombinedReviewResult(
|
|
734
|
-
plan_hash=plan_hash,
|
|
735
|
-
overall_verdict=overall,
|
|
736
|
-
cli_reviewers=cli_results,
|
|
737
|
-
orchestration=orch_result,
|
|
738
|
-
agents=agent_results,
|
|
739
|
-
timestamp=datetime.now().isoformat(),
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
# Merge display settings from both configs
|
|
743
|
-
display_settings = {**plan_settings.get("display", {}), **agent_settings.get("display", {})}
|
|
744
|
-
combined_settings = {"display": display_settings}
|
|
745
|
-
|
|
746
|
-
# Get current iteration number for folder naming
|
|
747
|
-
current_iteration = 1
|
|
748
|
-
if iteration_state:
|
|
749
|
-
current_iteration = iteration_state.get("current", 1)
|
|
750
|
-
|
|
751
|
-
# Create review folder with datetime and iteration in name
|
|
752
|
-
review_folder = get_review_folder_path(active_context.id, current_iteration, base)
|
|
753
|
-
review_folder.mkdir(parents=True, exist_ok=True)
|
|
754
|
-
log_info("cc-native-plan-review", f"Created review folder: {review_folder}")
|
|
755
|
-
|
|
756
|
-
review_file = write_combined_artifacts(
|
|
757
|
-
base, plan, combined_result, payload, combined_settings,
|
|
758
|
-
review_folder=review_folder,
|
|
759
|
-
iteration=current_iteration,
|
|
760
|
-
)
|
|
761
|
-
log_info("cc-native-plan-review", f"Saved review: {review_file}")
|
|
762
|
-
|
|
763
|
-
# Build inline review summary for additionalContext
|
|
764
|
-
inline_summary = build_inline_review_summary(combined_result)
|
|
765
|
-
|
|
766
|
-
context_parts = [inline_summary, f"\nFull review: `{review_file}`\n"]
|
|
767
|
-
|
|
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)
|
|
771
|
-
|
|
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
|
-
)
|
|
779
|
-
|
|
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")
|
|
795
|
-
|
|
796
|
-
# Handle iteration logic
|
|
797
|
-
needs_more_iterations = False
|
|
798
|
-
if iteration_state and reviews_dir:
|
|
799
|
-
# Update iteration state with this review result
|
|
800
|
-
iteration_state = update_iteration_state_in_context(reviews_dir, iteration_state, plan_hash, overall)
|
|
801
|
-
|
|
802
|
-
# Check if more iterations needed
|
|
803
|
-
if should_continue_iterating_context(iteration_state, review_score, agent_settings):
|
|
804
|
-
needs_more_iterations = True
|
|
805
|
-
# Increment iteration counter for next round
|
|
806
|
-
iteration_state["current"] = iteration_state.get("current", 1) + 1
|
|
807
|
-
# Save updated state for next iteration
|
|
808
|
-
save_iteration_state(reviews_dir, iteration_state)
|
|
809
|
-
else:
|
|
810
|
-
# Final iteration - increment current and save state
|
|
811
|
-
iteration_state["current"] = iteration_state.get("current", 1) + 1
|
|
812
|
-
# Also increment max by 1 to allow another review cycle if the user rejects
|
|
813
|
-
# the plan and requests changes. Without this, once iterations are exhausted,
|
|
814
|
-
# the hook would skip review entirely even if the user sent the
|
|
815
|
-
# planner back to revise. This ensures rejected plans can always be re-reviewed.
|
|
816
|
-
iteration_state["max"] = iteration_state.get("max", 1) + 1
|
|
817
|
-
save_iteration_state(reviews_dir, iteration_state)
|
|
818
|
-
|
|
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
|
-
)
|
|
833
|
-
|
|
834
|
-
if needs_more_iterations:
|
|
835
|
-
mark_plan_reviewed(session_id, plan_hash, "cc-native-plan-review", iteration_state, decision="deny")
|
|
836
|
-
current = iteration_state["current"] - 1 # Display the just-completed iteration
|
|
837
|
-
max_iter = iteration_state["max"]
|
|
838
|
-
remaining = max_iter - current
|
|
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}",
|
|
848
|
-
)
|
|
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}",
|
|
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)
|
|
863
|
-
|
|
864
|
-
return 0
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if __name__ == "__main__":
|
|
868
|
-
from base.hook_utils import run_hook
|
|
869
|
-
run_hook(main, "cc_native_plan_review")
|