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
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
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
|
+
* Configuration: _cc-native/plan-review.config.json -> planReview, agentReview
|
|
14
|
+
*
|
|
15
|
+
* Output: _output/cc-native/plans/{YYYY-MM-DD}/{slug}/reviews/
|
|
16
|
+
* - review.json (combined review data)
|
|
17
|
+
* - review.md (combined markdown)
|
|
18
|
+
* - plan.md (plan snapshot at review time)
|
|
19
|
+
* - reviewer-output/{reviewer}.json (individual reviewer results)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import * as os from "node:os";
|
|
25
|
+
import * as crypto from "node:crypto";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
loadHookInput,
|
|
29
|
+
runHookAsync,
|
|
30
|
+
logDebug,
|
|
31
|
+
logInfo,
|
|
32
|
+
logWarn,
|
|
33
|
+
logError,
|
|
34
|
+
logDiagnostic,
|
|
35
|
+
emitContext,
|
|
36
|
+
emitContextAndBlock,
|
|
37
|
+
} from "../../_shared/lib-ts/base/hook-utils.js";
|
|
38
|
+
import { isInternalCall } from "../../_shared/lib-ts/base/subprocess-utils.js";
|
|
39
|
+
import { getProjectRoot, getAiwcliDir, getContextReviewsDir, getContextDir, getReviewFolderPath } from "../../_shared/lib-ts/base/constants.js";
|
|
40
|
+
import { eprint } from "../../_shared/lib-ts/base/utils.js";
|
|
41
|
+
import { getContextBySessionId, getAllContexts } from "../../_shared/lib-ts/context/context-store.js";
|
|
42
|
+
|
|
43
|
+
import type {
|
|
44
|
+
AgentConfig,
|
|
45
|
+
OrchestratorConfig,
|
|
46
|
+
ReviewerResult,
|
|
47
|
+
CombinedReviewResult,
|
|
48
|
+
OrchestratorResult,
|
|
49
|
+
Verdict,
|
|
50
|
+
IterationState,
|
|
51
|
+
} from "../lib-ts/types.js";
|
|
52
|
+
import type { ContextState } from "../../_shared/lib-ts/types.js";
|
|
53
|
+
import {
|
|
54
|
+
REVIEW_SCHEMA,
|
|
55
|
+
DEFAULT_DISPLAY,
|
|
56
|
+
DEFAULT_SANITIZATION,
|
|
57
|
+
} from "../lib-ts/types.js";
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
isPlanAlreadyReviewed,
|
|
61
|
+
wasPlanPreviouslyDenied,
|
|
62
|
+
markPlanReviewed,
|
|
63
|
+
} from "../lib-ts/cc-native-state.js";
|
|
64
|
+
|
|
65
|
+
import { worstVerdict, computeReviewDecision } from "../lib-ts/verdict.js";
|
|
66
|
+
import { loadConfig, getDisplaySettings } from "../lib-ts/config.js";
|
|
67
|
+
import { runOrchestrator } from "../lib-ts/orchestrator.js";
|
|
68
|
+
import { aggregateAgents } from "../lib-ts/aggregate-agents.js";
|
|
69
|
+
import { debugLog } from "../lib-ts/debug.js";
|
|
70
|
+
import {
|
|
71
|
+
writeCombinedArtifacts,
|
|
72
|
+
buildInlineReviewSummary,
|
|
73
|
+
extractTopIssuesText,
|
|
74
|
+
buildHighIssuesDocument,
|
|
75
|
+
writeReviewTracker,
|
|
76
|
+
} from "../lib-ts/artifacts.js";
|
|
77
|
+
import type { ReviewTrackerEntry } from "../lib-ts/artifacts.js";
|
|
78
|
+
import { runAgentReview, runCodexReview, runGeminiReview } from "../lib-ts/reviewers/index.js";
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Hook Name
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const HOOK = "cc-native-plan-review";
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Inline Utilities (no TS export for these yet)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function findPlanFile(): string | null {
|
|
91
|
+
const plansDir = path.join(os.homedir(), ".claude", "plans");
|
|
92
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
93
|
+
const files = fs.readdirSync(plansDir)
|
|
94
|
+
.filter(f => f.endsWith(".md"))
|
|
95
|
+
.map(f => {
|
|
96
|
+
const p = path.join(plansDir, f);
|
|
97
|
+
return { path: p, mtime: fs.statSync(p).mtimeMs };
|
|
98
|
+
})
|
|
99
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
100
|
+
return files.length > 0 ? files[0]!.path : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function computePlanHash(content: string): string {
|
|
104
|
+
return crypto.createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function skipWithInfo(reason: string): void {
|
|
108
|
+
logInfo(HOOK, `Skipping: ${reason}`);
|
|
109
|
+
emitContext(`[Plan Review Skipped] ${reason}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractTopIssuesForTracker(
|
|
113
|
+
combined: CombinedReviewResult,
|
|
114
|
+
maxCount = 5,
|
|
115
|
+
): string[] {
|
|
116
|
+
const allReviewers = [
|
|
117
|
+
...Object.values(combined.cli_reviewers),
|
|
118
|
+
...Object.values(combined.agents),
|
|
119
|
+
];
|
|
120
|
+
const issues: string[] = [];
|
|
121
|
+
for (const r of allReviewers) {
|
|
122
|
+
if (!r.data) continue;
|
|
123
|
+
const issueList = r.data.issues as Array<Record<string, unknown>> | undefined;
|
|
124
|
+
if (!issueList) continue;
|
|
125
|
+
for (const issue of issueList) {
|
|
126
|
+
if (issue.severity === "high") {
|
|
127
|
+
const text = String(issue.issue ?? "").trim();
|
|
128
|
+
if (text) {
|
|
129
|
+
issues.push(`[${r.name}] ${text}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (issues.length >= maxCount) break;
|
|
134
|
+
}
|
|
135
|
+
return issues.slice(0, maxCount);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Graduation Logic
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Determine which agents should graduate based on their review results.
|
|
144
|
+
* Graduation criteria: verdict === "pass" OR zero high-severity issues.
|
|
145
|
+
* Agents with "skip"/"error" do NOT graduate (no signal).
|
|
146
|
+
*/
|
|
147
|
+
function computeGraduated(agentResults: Record<string, ReviewerResult>): string[] {
|
|
148
|
+
const graduated: string[] = [];
|
|
149
|
+
for (const [name, result] of Object.entries(agentResults)) {
|
|
150
|
+
if (result.verdict === "skip" || result.verdict === "error") continue;
|
|
151
|
+
if (result.verdict === "pass") { graduated.push(name); continue; }
|
|
152
|
+
const issues = Array.isArray(result.data?.issues)
|
|
153
|
+
? (result.data.issues as Array<{ severity?: string }>) : [];
|
|
154
|
+
if (issues.filter(i => i.severity === "high").length === 0) {
|
|
155
|
+
graduated.push(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return graduated;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Load the set of graduated agent names from previous iterations.
|
|
163
|
+
* Returns empty set on iteration 1 (no iteration.json exists).
|
|
164
|
+
*/
|
|
165
|
+
function loadGraduatedSet(reviewsDir: string): Set<string> {
|
|
166
|
+
const existing = loadIterationState(reviewsDir);
|
|
167
|
+
return new Set(existing?.graduated ?? []);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Default Configuration
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
const DEFAULT_AGENTS: Array<{ name: string; model: string; focus: string; enabled: boolean; categories: string[] }> = [
|
|
175
|
+
{ name: "handoff-readiness", model: "sonnet", focus: "fresh context execution readiness", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
176
|
+
{ name: "clarity-auditor", model: "sonnet", focus: "communication clarity and execution readiness", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
177
|
+
{ name: "skeptic", model: "sonnet", focus: "problem-solution alignment and assumption validation", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
178
|
+
{ name: "documentation-philosophy", model: "sonnet", focus: "knowledge capture and documentation placement", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
179
|
+
{ name: "risk-premortem", model: "sonnet", focus: "pre-mortem failure analysis", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
180
|
+
{ name: "risk-fmea", model: "sonnet", focus: "systematic failure mode analysis", enabled: true, categories: ["code", "infrastructure", "design"] },
|
|
181
|
+
{ name: "risk-dependency", model: "sonnet", focus: "dependency chain and blast radius analysis", enabled: true, categories: ["code", "infrastructure"] },
|
|
182
|
+
{ name: "risk-reversibility", model: "sonnet", focus: "decision reversibility and optionality", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
183
|
+
{ name: "completeness-gaps", model: "sonnet", focus: "structural gap analysis", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
184
|
+
{ name: "completeness-feasibility", model: "sonnet", focus: "feasibility and resource analysis", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
185
|
+
{ name: "completeness-ordering", model: "sonnet", focus: "step ordering and critical path analysis", enabled: true, categories: ["code", "infrastructure", "design"] },
|
|
186
|
+
{ name: "arch-structure", model: "sonnet", focus: "coupling, cohesion, and boundary analysis", enabled: true, categories: ["code", "infrastructure", "design"] },
|
|
187
|
+
{ name: "arch-evolution", model: "sonnet", focus: "evolutionary architecture and change amplification", enabled: true, categories: ["code", "infrastructure", "design"] },
|
|
188
|
+
{ name: "arch-patterns", model: "sonnet", focus: "pattern selection and technology fit", enabled: true, categories: ["code", "infrastructure"] },
|
|
189
|
+
{ name: "verify-coverage", model: "sonnet", focus: "verification coverage mapping", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
190
|
+
{ name: "verify-strength", model: "sonnet", focus: "test quality and mutation analysis", enabled: true, categories: ["code", "infrastructure"] },
|
|
191
|
+
{ name: "tradeoff-costs", model: "sonnet", focus: "opportunity cost and capability sacrifice", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
192
|
+
{ name: "tradeoff-stakeholders", model: "sonnet", focus: "stakeholder impact and cost-benefit asymmetry", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
193
|
+
{ name: "scope-boundary", model: "sonnet", focus: "scope drift and boundary enforcement", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
194
|
+
{ name: "hidden-complexity", model: "sonnet", focus: "understated complexity and hidden difficulty", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
195
|
+
{ name: "simplicity-guardian", model: "sonnet", focus: "over-engineering and unnecessary complexity", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
196
|
+
{ name: "devils-advocate", model: "sonnet", focus: "contrarian analysis and reductio ad absurdum", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
197
|
+
{ name: "assumption-tracer", model: "sonnet", focus: "dependency chains and foundational assumptions", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
198
|
+
{ name: "incremental-delivery", model: "sonnet", focus: "incremental delivery and vertical slicing", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
199
|
+
{ name: "constraint-validator", model: "sonnet", focus: "constraint identification and satisfaction", enabled: true, categories: ["code", "infrastructure", "documentation", "design", "research", "life", "business"] },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const DEFAULT_ORCHESTRATOR: { enabled: boolean; model: string; timeout: number } = { enabled: true, model: "opus", timeout: 60 };
|
|
203
|
+
const DEFAULT_AGENT_MODEL = "sonnet";
|
|
204
|
+
|
|
205
|
+
const DEFAULT_REVIEW_ITERATIONS: Record<string, number> = {
|
|
206
|
+
simple: 1,
|
|
207
|
+
medium: 2,
|
|
208
|
+
high: 2,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const DEFAULT_AGENT_SELECTION: Record<string, unknown> = {
|
|
212
|
+
simple: { min: 3, max: 3 },
|
|
213
|
+
medium: { min: 8, max: 8 },
|
|
214
|
+
high: { min: 12, max: 12 },
|
|
215
|
+
fallbackCount: 3,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const DEFAULT_COMPLEXITY_CATEGORIES = ["code", "infrastructure", "documentation", "life", "business", "design", "research"];
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Mandatory Agent Resolution
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function resolveMandatoryAgents(
|
|
225
|
+
configValue: unknown,
|
|
226
|
+
complexity: string,
|
|
227
|
+
): Set<string> {
|
|
228
|
+
if (Array.isArray(configValue)) {
|
|
229
|
+
return new Set(configValue as string[]);
|
|
230
|
+
}
|
|
231
|
+
if (!configValue || typeof configValue !== "object") {
|
|
232
|
+
return new Set(["handoff-readiness", "clarity-auditor", "skeptic"]);
|
|
233
|
+
}
|
|
234
|
+
const cfg = configValue as Record<string, string[]>;
|
|
235
|
+
const names = new Set(cfg.always ?? []);
|
|
236
|
+
if (complexity === "medium" || complexity === "high") {
|
|
237
|
+
for (const n of cfg["medium+"] ?? []) names.add(n);
|
|
238
|
+
}
|
|
239
|
+
if (complexity === "high") {
|
|
240
|
+
for (const n of cfg.high ?? []) names.add(n);
|
|
241
|
+
}
|
|
242
|
+
return names;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Context Lookup
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function getActiveContextForReview(sessionId: string, projectRoot: string): ContextState | null {
|
|
250
|
+
// Strategy 1: By session_id
|
|
251
|
+
const ctx = getContextBySessionId(sessionId, projectRoot);
|
|
252
|
+
if (ctx) {
|
|
253
|
+
logInfo(HOOK, `Found context by session_id: ${ctx.id}`);
|
|
254
|
+
return ctx;
|
|
255
|
+
}
|
|
256
|
+
// Strategy 2: Single planning context
|
|
257
|
+
const allActive = getAllContexts("active", projectRoot);
|
|
258
|
+
const planning = allActive.filter(c => c.mode === "active" || c.mode === "has_plan");
|
|
259
|
+
if (planning.length === 1) {
|
|
260
|
+
logInfo(HOOK, `Found single planning context: ${planning[0]!.id}`);
|
|
261
|
+
return planning[0]!;
|
|
262
|
+
}
|
|
263
|
+
if (planning.length > 1) {
|
|
264
|
+
logWarn(HOOK, `Multiple planning contexts (${planning.length}), cannot determine which to use`);
|
|
265
|
+
} else if (allActive.length > 0) {
|
|
266
|
+
logInfo(HOOK, `Found ${allActive.length} active context(s) but none in planning mode`);
|
|
267
|
+
} else {
|
|
268
|
+
logInfo(HOOK, "No active contexts found");
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Iteration State
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
function loadIterationState(reviewsDir: string): IterationState | null {
|
|
278
|
+
const iterationFile = path.join(reviewsDir, "iteration.json");
|
|
279
|
+
if (!fs.existsSync(iterationFile)) return null;
|
|
280
|
+
try {
|
|
281
|
+
return JSON.parse(fs.readFileSync(iterationFile, "utf-8")) as IterationState;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
logError(HOOK, `Failed to load iteration state: ${e}`);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function saveIterationState(reviewsDir: string, state: IterationState & { schema_version?: string }): boolean {
|
|
289
|
+
const iterationFile = path.join(reviewsDir, "iteration.json");
|
|
290
|
+
try {
|
|
291
|
+
fs.mkdirSync(reviewsDir, { recursive: true });
|
|
292
|
+
state.schema_version = "1.0.0";
|
|
293
|
+
fs.writeFileSync(iterationFile, JSON.stringify(state, null, 2), "utf-8");
|
|
294
|
+
return true;
|
|
295
|
+
} catch (e) {
|
|
296
|
+
logError(HOOK, `Failed to save iteration state: ${e}`);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getIterationStateFromContext(
|
|
302
|
+
reviewsDir: string,
|
|
303
|
+
complexity: string,
|
|
304
|
+
config?: Record<string, unknown>,
|
|
305
|
+
): IterationState {
|
|
306
|
+
const existing = loadIterationState(reviewsDir);
|
|
307
|
+
if (existing) return existing;
|
|
308
|
+
const reviewIterations: Record<string, number> = { ...DEFAULT_REVIEW_ITERATIONS };
|
|
309
|
+
if (config) {
|
|
310
|
+
const overrides = config.reviewIterations;
|
|
311
|
+
if (overrides && typeof overrides === "object") {
|
|
312
|
+
Object.assign(reviewIterations, overrides);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
current: 1,
|
|
317
|
+
max: reviewIterations[complexity] ?? 1,
|
|
318
|
+
complexity,
|
|
319
|
+
history: [],
|
|
320
|
+
graduated: [],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Settings Loading
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
function loadSettings(projDir: string): Record<string, any> {
|
|
329
|
+
const defaults: Record<string, any> = {
|
|
330
|
+
planReview: {
|
|
331
|
+
enabled: true,
|
|
332
|
+
reviewers: {
|
|
333
|
+
codex: { enabled: true, model: "", timeout: 120 },
|
|
334
|
+
gemini: { enabled: false, model: "", timeout: 120 },
|
|
335
|
+
},
|
|
336
|
+
display: { ...DEFAULT_DISPLAY },
|
|
337
|
+
},
|
|
338
|
+
agentReview: {
|
|
339
|
+
enabled: true,
|
|
340
|
+
orchestrator: { ...DEFAULT_ORCHESTRATOR },
|
|
341
|
+
timeout: 180,
|
|
342
|
+
highIssueThreshold: 3,
|
|
343
|
+
legacyMode: false,
|
|
344
|
+
display: { ...DEFAULT_DISPLAY },
|
|
345
|
+
agentSelection: { ...DEFAULT_AGENT_SELECTION },
|
|
346
|
+
agentDefaults: { model: DEFAULT_AGENT_MODEL },
|
|
347
|
+
complexityCategories: [...DEFAULT_COMPLEXITY_CATEGORIES],
|
|
348
|
+
sanitization: { ...DEFAULT_SANITIZATION },
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const config = loadConfig(projDir);
|
|
353
|
+
if (!config || Object.keys(config).length === 0) return defaults;
|
|
354
|
+
|
|
355
|
+
// Merge planReview
|
|
356
|
+
const planReview = config.planReview ?? {};
|
|
357
|
+
const mergedPlan = { ...defaults.planReview, ...planReview };
|
|
358
|
+
if (planReview.reviewers) {
|
|
359
|
+
mergedPlan.reviewers = { ...defaults.planReview.reviewers, ...planReview.reviewers };
|
|
360
|
+
}
|
|
361
|
+
mergedPlan.display = getDisplaySettings(config, "planReview");
|
|
362
|
+
|
|
363
|
+
// Merge agentReview
|
|
364
|
+
const agentReview = (config as Record<string, unknown>).agentReview ?? {};
|
|
365
|
+
const mergedAgent = { ...defaults.agentReview, ...agentReview };
|
|
366
|
+
if (!mergedAgent.orchestrator || typeof mergedAgent.orchestrator !== "object") {
|
|
367
|
+
mergedAgent.orchestrator = { ...DEFAULT_ORCHESTRATOR };
|
|
368
|
+
} else {
|
|
369
|
+
mergedAgent.orchestrator = { ...DEFAULT_ORCHESTRATOR, ...mergedAgent.orchestrator };
|
|
370
|
+
}
|
|
371
|
+
mergedAgent.display = getDisplaySettings(config, "agentReview");
|
|
372
|
+
const configRecord = config as Record<string, unknown>;
|
|
373
|
+
mergedAgent.agentSelection = { ...DEFAULT_AGENT_SELECTION, ...((configRecord.agentSelection as Record<string, unknown>) ?? {}) };
|
|
374
|
+
mergedAgent.agentDefaults = { model: DEFAULT_AGENT_MODEL, ...((configRecord.agentDefaults as Record<string, unknown>) ?? {}) };
|
|
375
|
+
mergedAgent.complexityCategories = (configRecord.complexityCategories as string[]) ?? [...DEFAULT_COMPLEXITY_CATEGORIES];
|
|
376
|
+
mergedAgent.sanitization = { ...DEFAULT_SANITIZATION, ...((configRecord.sanitization as Record<string, unknown>) ?? {}) };
|
|
377
|
+
mergedAgent.reviewIterations = { ...DEFAULT_REVIEW_ITERATIONS, ...agentReview.reviewIterations ?? {} };
|
|
378
|
+
|
|
379
|
+
return { planReview: mergedPlan, agentReview: mergedAgent };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function loadAgentLibrary(
|
|
383
|
+
projDir: string,
|
|
384
|
+
settings?: Record<string, any>,
|
|
385
|
+
): AgentConfig[] {
|
|
386
|
+
const agentsData = aggregateAgents(path.join(projDir, "_cc-native", "agents"));
|
|
387
|
+
const defaultModel = settings?.agentDefaults?.model ?? DEFAULT_AGENT_MODEL;
|
|
388
|
+
|
|
389
|
+
if (!agentsData || agentsData.length === 0) {
|
|
390
|
+
logInfo(HOOK, "No agents found in frontmatter, using defaults");
|
|
391
|
+
return DEFAULT_AGENTS.map(a => ({
|
|
392
|
+
name: a.name,
|
|
393
|
+
model: a.model ?? defaultModel,
|
|
394
|
+
focus: a.focus ?? "general review",
|
|
395
|
+
enabled: a.enabled ?? true,
|
|
396
|
+
categories: a.categories ?? ["code"],
|
|
397
|
+
description: "",
|
|
398
|
+
system_prompt: "",
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return agentsData.filter(a => a.name !== "plan-orchestrator");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Main Hook
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
async function main(): Promise<void> {
|
|
410
|
+
logInfo(HOOK, "Unified hook started (PreToolUse)");
|
|
411
|
+
|
|
412
|
+
if (isInternalCall()) {
|
|
413
|
+
logDebug(HOOK, "Skipping: internal subprocess call");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const payload = loadHookInput();
|
|
418
|
+
if (!payload) {
|
|
419
|
+
skipWithInfo("Invalid JSON input from Claude Code");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const toolName = payload.tool_name;
|
|
424
|
+
logDebug(HOOK, `tool_name: ${toolName}`);
|
|
425
|
+
|
|
426
|
+
if (toolName !== "ExitPlanMode") {
|
|
427
|
+
logDebug(HOOK, "Skipping: not ExitPlanMode");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const sessionId = String(payload.session_id ?? "unknown");
|
|
432
|
+
const base = getProjectRoot(payload.cwd);
|
|
433
|
+
const aiwcliDir = getAiwcliDir(base);
|
|
434
|
+
const settings = loadSettings(aiwcliDir);
|
|
435
|
+
|
|
436
|
+
const planSettings = settings.planReview ?? {};
|
|
437
|
+
const agentSettings = settings.agentReview ?? {};
|
|
438
|
+
|
|
439
|
+
const planReviewEnabled = planSettings.enabled ?? true;
|
|
440
|
+
const agentReviewEnabled = agentSettings.enabled ?? true;
|
|
441
|
+
|
|
442
|
+
if (!planReviewEnabled && !agentReviewEnabled) {
|
|
443
|
+
logInfo(HOOK, "Skipping: both plan and agent review disabled");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Find and read plan
|
|
448
|
+
const planPath = findPlanFile();
|
|
449
|
+
if (!planPath) {
|
|
450
|
+
skipWithInfo("No plan file found in ~/.claude/plans/. The plan may not have been written yet.");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let plan: string;
|
|
455
|
+
try {
|
|
456
|
+
plan = fs.readFileSync(planPath, "utf-8").trim();
|
|
457
|
+
} catch (e) {
|
|
458
|
+
skipWithInfo(`Failed to read plan file: ${e}`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!plan) {
|
|
463
|
+
skipWithInfo("Plan file exists but is empty.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
logInfo(HOOK, `Found plan at: ${planPath}`);
|
|
468
|
+
logDebug(HOOK, `Plan length: ${plan.length} chars`);
|
|
469
|
+
|
|
470
|
+
const planHash = computePlanHash(plan);
|
|
471
|
+
logDiagnostic(HOOK, "receive", `plan_size=${plan.length}, session=${sessionId.slice(0, 8)}`, {
|
|
472
|
+
inputs: { plan_hash: planHash, plan_size: plan.length, session_id: sessionId.slice(0, 12) },
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Find active context
|
|
476
|
+
const activeContext = getActiveContextForReview(sessionId, base);
|
|
477
|
+
if (!activeContext) {
|
|
478
|
+
skipWithInfo("No active planning context found for this session.");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const contextId = activeContext.id;
|
|
483
|
+
const reviewsDir = path.join(getContextReviewsDir(contextId, base), "cc-native");
|
|
484
|
+
logDebug(HOOK, `Using context reviews dir: ${reviewsDir}`);
|
|
485
|
+
|
|
486
|
+
const contextPath = getContextDir(contextId, base);
|
|
487
|
+
logDebug(HOOK, `Context path for debug: ${contextPath}`);
|
|
488
|
+
|
|
489
|
+
// Plan-hash deduplication
|
|
490
|
+
logDebug(HOOK, `Plan hash: ${planHash}`);
|
|
491
|
+
if (isPlanAlreadyReviewed(sessionId, planHash, base)) {
|
|
492
|
+
if (wasPlanPreviouslyDenied(sessionId, planHash, base)) {
|
|
493
|
+
emitContextAndBlock(
|
|
494
|
+
"[Plan Review] Plan content unchanged since last review which found issues.",
|
|
495
|
+
"Plan unchanged since denial. Modify the plan to address review findings, then attempt ExitPlanMode again.",
|
|
496
|
+
);
|
|
497
|
+
return;
|
|
498
|
+
} else {
|
|
499
|
+
skipWithInfo("Plan already reviewed and approved (same hash).");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Early iteration check: if we've exhausted max iterations, allow plan through
|
|
505
|
+
const earlyIterState = loadIterationState(reviewsDir);
|
|
506
|
+
if (earlyIterState && earlyIterState.current > earlyIterState.max) {
|
|
507
|
+
skipWithInfo(`Max review iterations reached (${earlyIterState.current - 1}/${earlyIterState.max}), allowing plan through.`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Initialize result containers
|
|
512
|
+
const cliResults: Record<string, ReviewerResult> = {};
|
|
513
|
+
let orchResult: OrchestratorResult | null = null;
|
|
514
|
+
const agentResults: Record<string, ReviewerResult> = {};
|
|
515
|
+
let allVerdicts: Verdict[] = [];
|
|
516
|
+
let iterationState: IterationState | null = null;
|
|
517
|
+
let detectedComplexity = "medium";
|
|
518
|
+
|
|
519
|
+
// ============================================
|
|
520
|
+
// PHASE 1 & 2: CLI Reviewers + Orchestrator (PARALLEL)
|
|
521
|
+
// ============================================
|
|
522
|
+
const reviewersConfig = planReviewEnabled ? (planSettings.reviewers ?? {}) : {};
|
|
523
|
+
const codexEnabled = planReviewEnabled && (reviewersConfig.codex?.enabled ?? true);
|
|
524
|
+
const geminiEnabled = planReviewEnabled && (reviewersConfig.gemini?.enabled ?? false);
|
|
525
|
+
|
|
526
|
+
// Load graduated agents from previous iterations (empty on iteration 1)
|
|
527
|
+
const graduatedSet = loadGraduatedSet(reviewsDir);
|
|
528
|
+
if (graduatedSet.size > 0) {
|
|
529
|
+
logInfo(HOOK, `Graduated agents from previous iterations: ${[...graduatedSet].sort().join(", ")}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const agentLibrary = agentReviewEnabled ? loadAgentLibrary(aiwcliDir, agentSettings) : [];
|
|
533
|
+
const originalAgentCount = agentLibrary.length;
|
|
534
|
+
const enabledAgents = agentLibrary.filter(a => !graduatedSet.has(a.name));
|
|
535
|
+
const timeout = typeof agentSettings.timeout === "number" ? agentSettings.timeout : 120;
|
|
536
|
+
const legacyMode = agentSettings.legacyMode === true;
|
|
537
|
+
|
|
538
|
+
const orchSettings = agentSettings.orchestrator ?? DEFAULT_ORCHESTRATOR;
|
|
539
|
+
const orchestratorConfig: OrchestratorConfig = {
|
|
540
|
+
enabled: (orchSettings.enabled ?? true) && agentReviewEnabled,
|
|
541
|
+
model: orchSettings.model ?? "haiku",
|
|
542
|
+
timeout: orchSettings.timeout ?? 30,
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const mandatoryConfig = agentSettings.mandatoryAgents ?? ["handoff-readiness", "clarity-auditor", "skeptic"];
|
|
546
|
+
const alwaysMandatory = resolveMandatoryAgents(mandatoryConfig, "simple");
|
|
547
|
+
let mandatoryNames = alwaysMandatory;
|
|
548
|
+
|
|
549
|
+
logDebug(HOOK, `Codex enabled: ${codexEnabled}, Gemini enabled: ${geminiEnabled}`);
|
|
550
|
+
logDebug(HOOK, `Agent library: ${agentLibrary.map(a => a.name)}`);
|
|
551
|
+
logDebug(HOOK, `Mandatory agents: ${[...mandatoryNames].sort()}`);
|
|
552
|
+
logDebug(HOOK, `Orchestrator enabled: ${orchestratorConfig.enabled}`);
|
|
553
|
+
|
|
554
|
+
// Build phase 1 tasks as promises
|
|
555
|
+
const phase1Promises: Array<{ name: string; promise: Promise<ReviewerResult | OrchestratorResult> }> = [];
|
|
556
|
+
|
|
557
|
+
if (codexEnabled) {
|
|
558
|
+
phase1Promises.push({
|
|
559
|
+
name: "codex",
|
|
560
|
+
promise: runCodexReview(plan, REVIEW_SCHEMA, planSettings),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (geminiEnabled) {
|
|
564
|
+
phase1Promises.push({
|
|
565
|
+
name: "gemini",
|
|
566
|
+
promise: runGeminiReview(plan, REVIEW_SCHEMA, planSettings),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (orchestratorConfig.enabled && enabledAgents.length > 0 && !legacyMode) {
|
|
570
|
+
phase1Promises.push({
|
|
571
|
+
name: "orchestrator",
|
|
572
|
+
promise: runOrchestrator(plan, enabledAgents, orchestratorConfig, agentSettings, alwaysMandatory),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
logInfo(HOOK, `=== PHASE 1: Running ${phase1Promises.length} tasks in parallel ===`);
|
|
577
|
+
|
|
578
|
+
const phase1Results: Record<string, ReviewerResult | OrchestratorResult> = {};
|
|
579
|
+
if (phase1Promises.length > 0) {
|
|
580
|
+
const results = await Promise.allSettled(
|
|
581
|
+
phase1Promises.map(async ({ name, promise }) => {
|
|
582
|
+
const result = await promise;
|
|
583
|
+
return { name, result };
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
for (const [i, r] of results.entries()) {
|
|
587
|
+
if (r.status === "fulfilled") {
|
|
588
|
+
phase1Results[r.value.name] = r.value.result;
|
|
589
|
+
logInfo(HOOK, `${r.value.name} completed`);
|
|
590
|
+
} else {
|
|
591
|
+
const failedName = phase1Promises[i]?.name ?? "unknown";
|
|
592
|
+
logError(HOOK, `${failedName} failed: ${r.reason}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Collect CLI results
|
|
598
|
+
if (phase1Results.codex) cliResults.codex = phase1Results.codex as ReviewerResult;
|
|
599
|
+
if (phase1Results.gemini) cliResults.gemini = phase1Results.gemini as ReviewerResult;
|
|
600
|
+
if (phase1Results.orchestrator) orchResult = phase1Results.orchestrator as OrchestratorResult;
|
|
601
|
+
|
|
602
|
+
// ============================================
|
|
603
|
+
// PHASE 2: Agent Selection
|
|
604
|
+
// ============================================
|
|
605
|
+
if (agentReviewEnabled) {
|
|
606
|
+
logInfo(HOOK, "=== PHASE 2: Agent Selection ===");
|
|
607
|
+
|
|
608
|
+
let selectedAgents: AgentConfig[] = [];
|
|
609
|
+
const fallbackByComplexity = agentSettings.fallbackByComplexity ?? { simple: 0, medium: 5, high: 9 };
|
|
610
|
+
|
|
611
|
+
if (enabledAgents.length > 0) {
|
|
612
|
+
let mandatoryAgents = enabledAgents.filter(a => mandatoryNames.has(a.name));
|
|
613
|
+
let nonMandatory = enabledAgents.filter(a => !mandatoryNames.has(a.name));
|
|
614
|
+
|
|
615
|
+
logDebug(HOOK, `Mandatory agents: ${mandatoryAgents.map(a => a.name)}`);
|
|
616
|
+
logDebug(HOOK, `Non-mandatory pool: ${nonMandatory.length} agents`);
|
|
617
|
+
|
|
618
|
+
if (orchResult && !legacyMode) {
|
|
619
|
+
detectedComplexity = orchResult.complexity;
|
|
620
|
+
|
|
621
|
+
// Phase 2: Recompute mandatory with actual complexity
|
|
622
|
+
mandatoryNames = resolveMandatoryAgents(mandatoryConfig, detectedComplexity);
|
|
623
|
+
mandatoryAgents = enabledAgents.filter(a => mandatoryNames.has(a.name));
|
|
624
|
+
nonMandatory = enabledAgents.filter(a => !mandatoryNames.has(a.name));
|
|
625
|
+
|
|
626
|
+
const orchSelectedNames = new Set(
|
|
627
|
+
orchResult.selected_agents.filter(n => !mandatoryNames.has(n)),
|
|
628
|
+
);
|
|
629
|
+
let orchSelected = nonMandatory.filter(a => orchSelectedNames.has(a.name));
|
|
630
|
+
|
|
631
|
+
logDebug(HOOK, `Orchestrator selected (non-mandatory): ${orchSelected.map(a => a.name)}`);
|
|
632
|
+
|
|
633
|
+
// Warn if orchestrator returned unknown names
|
|
634
|
+
const knownNames = new Set(nonMandatory.map(a => a.name));
|
|
635
|
+
const unmatched = [...orchSelectedNames].filter(n => !knownNames.has(n));
|
|
636
|
+
if (unmatched.length > 0) {
|
|
637
|
+
logWarn(HOOK, `Orchestrator selected unknown agents: ${unmatched}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Enforce minimum agent count
|
|
641
|
+
const minAdditional = fallbackByComplexity[detectedComplexity] ?? 5;
|
|
642
|
+
if (orchSelected.length < minAdditional && nonMandatory.length > 0) {
|
|
643
|
+
const remaining = nonMandatory.filter(a => !orchSelected.includes(a));
|
|
644
|
+
const topUpCount = Math.min(minAdditional - orchSelected.length, remaining.length);
|
|
645
|
+
if (topUpCount > 0) {
|
|
646
|
+
// Shuffle and take random sample
|
|
647
|
+
const shuffled = [...remaining].sort(() => Math.random() - 0.5);
|
|
648
|
+
const topUp = shuffled.slice(0, topUpCount);
|
|
649
|
+
orchSelected = [...orchSelected, ...topUp];
|
|
650
|
+
logDebug(HOOK, `Topped up ${topUpCount} agents to meet ${detectedComplexity} minimum: ${topUp.map(a => a.name)}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
selectedAgents = [...mandatoryAgents, ...orchSelected];
|
|
655
|
+
logInfo(HOOK, `Final selection: ${selectedAgents.length} agents (${mandatoryAgents.length} mandatory + ${orchSelected.length} additional)`);
|
|
656
|
+
} else {
|
|
657
|
+
logInfo(HOOK, "Running in legacy mode (all enabled agents)");
|
|
658
|
+
detectedComplexity = "medium";
|
|
659
|
+
mandatoryNames = resolveMandatoryAgents(mandatoryConfig, detectedComplexity);
|
|
660
|
+
selectedAgents = enabledAgents;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
logDiagnostic(HOOK, "decide", `Selected ${selectedAgents.length} agents, complexity=${detectedComplexity}`, {
|
|
665
|
+
decision: "agents_selected",
|
|
666
|
+
reasoning: `orchestrator=${orchResult !== null}, legacy=${legacyMode}`,
|
|
667
|
+
inputs: {
|
|
668
|
+
agents: selectedAgents.map(a => a.name),
|
|
669
|
+
complexity: detectedComplexity,
|
|
670
|
+
mandatory_count: selectedAgents.filter(a => mandatoryNames.has(a.name)).length,
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Initialize iteration state
|
|
675
|
+
if (reviewsDir) {
|
|
676
|
+
iterationState = getIterationStateFromContext(reviewsDir, detectedComplexity, agentSettings);
|
|
677
|
+
logDebug(HOOK, `Iteration state: ${iterationState.current}/${iterationState.max} (${detectedComplexity})`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// PHASE 3: Run selected agents in parallel
|
|
681
|
+
if (selectedAgents.length > 0) {
|
|
682
|
+
logInfo(HOOK, "=== PHASE 3: Agent Reviews ===");
|
|
683
|
+
logInfo(HOOK, `Launching ${selectedAgents.length} agents in parallel`);
|
|
684
|
+
|
|
685
|
+
debugLog(contextPath, sessionId, "hook", "agent_review_start", {
|
|
686
|
+
agents: selectedAgents.map(a => a.name),
|
|
687
|
+
timeout,
|
|
688
|
+
complexity: detectedComplexity,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const agentPromises = selectedAgents.map(async agent => {
|
|
692
|
+
const result = await runAgentReview(plan, agent, REVIEW_SCHEMA, timeout, contextPath, sessionId);
|
|
693
|
+
return { agent, result };
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const agentSettled = await Promise.allSettled(agentPromises);
|
|
697
|
+
for (const [i, r] of agentSettled.entries()) {
|
|
698
|
+
if (r.status === "fulfilled") {
|
|
699
|
+
const { agent, result } = r.value;
|
|
700
|
+
agentResults[agent.name] = result;
|
|
701
|
+
logInfo(HOOK, `${agent.name} completed with verdict: ${result.verdict}`);
|
|
702
|
+
} else {
|
|
703
|
+
const failedAgent = selectedAgents[i]!;
|
|
704
|
+
logError(HOOK, `${failedAgent.name} failed with exception: ${r.reason}`);
|
|
705
|
+
agentResults[failedAgent.name] = {
|
|
706
|
+
name: failedAgent.name,
|
|
707
|
+
ok: false,
|
|
708
|
+
verdict: "error",
|
|
709
|
+
data: {},
|
|
710
|
+
raw: "",
|
|
711
|
+
err: String(r.reason),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ============================================
|
|
719
|
+
// Persist newly graduated agents (before verdict overrides)
|
|
720
|
+
// ============================================
|
|
721
|
+
const newlyGraduated = computeGraduated(agentResults);
|
|
722
|
+
if (newlyGraduated.length > 0) {
|
|
723
|
+
logInfo(HOOK, `Newly graduated agents: ${newlyGraduated.join(", ")}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ============================================
|
|
727
|
+
// Per-agent high-severity threshold: override verdict to "fail"
|
|
728
|
+
// ============================================
|
|
729
|
+
const highIssueThreshold = typeof agentSettings.highIssueThreshold === "number" ? agentSettings.highIssueThreshold : 3;
|
|
730
|
+
allVerdicts = [];
|
|
731
|
+
|
|
732
|
+
for (const r of [...Object.values(cliResults), ...Object.values(agentResults)]) {
|
|
733
|
+
if (!r.verdict || r.verdict === "skip" || r.verdict === "error") continue;
|
|
734
|
+
const issues = Array.isArray(r.data?.issues) ? r.data.issues as Array<{ severity?: string }> : [];
|
|
735
|
+
const agentHigh = issues.filter(i => i.severity === "high").length;
|
|
736
|
+
let verdict = r.verdict;
|
|
737
|
+
if (agentHigh >= highIssueThreshold) {
|
|
738
|
+
logInfo(HOOK, `${r.name}: verdict overridden to 'fail' (${agentHigh} high issues >= ${highIssueThreshold})`);
|
|
739
|
+
verdict = "fail";
|
|
740
|
+
r.verdict = verdict;
|
|
741
|
+
}
|
|
742
|
+
allVerdicts.push(verdict);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ============================================
|
|
746
|
+
// PHASE 4: Generate Combined Output
|
|
747
|
+
// ============================================
|
|
748
|
+
logInfo(HOOK, "=== PHASE 4: Generate Output ===");
|
|
749
|
+
|
|
750
|
+
if (Object.keys(cliResults).length === 0 && Object.keys(agentResults).length === 0) {
|
|
751
|
+
if (graduatedSet.size > 0 && originalAgentCount > 0) {
|
|
752
|
+
skipWithInfo("All agent reviewers graduated from previous iterations — no review needed.");
|
|
753
|
+
} else {
|
|
754
|
+
skipWithInfo("All reviewers failed to produce results. Check stderr logs for details.");
|
|
755
|
+
}
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const overall = allVerdicts.length > 0 ? worstVerdict(allVerdicts) : "pass";
|
|
760
|
+
|
|
761
|
+
const combinedResult: CombinedReviewResult = {
|
|
762
|
+
plan_hash: planHash,
|
|
763
|
+
overall_verdict: overall,
|
|
764
|
+
cli_reviewers: cliResults,
|
|
765
|
+
orchestration: orchResult,
|
|
766
|
+
agents: agentResults,
|
|
767
|
+
timestamp: new Date().toISOString(),
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const displaySettings = {
|
|
771
|
+
...(planSettings.display ?? {}),
|
|
772
|
+
...(agentSettings.display ?? {}),
|
|
773
|
+
};
|
|
774
|
+
const combinedSettings = { display: displaySettings };
|
|
775
|
+
|
|
776
|
+
// Get current iteration number
|
|
777
|
+
const currentIteration = iterationState?.current ?? 1;
|
|
778
|
+
|
|
779
|
+
// Create review folder
|
|
780
|
+
const reviewFolder = getReviewFolderPath(contextId, currentIteration, base);
|
|
781
|
+
fs.mkdirSync(reviewFolder, { recursive: true });
|
|
782
|
+
logInfo(HOOK, `Created review folder: ${reviewFolder}`);
|
|
783
|
+
|
|
784
|
+
const reviewFile = writeCombinedArtifacts(
|
|
785
|
+
base,
|
|
786
|
+
plan,
|
|
787
|
+
combinedResult,
|
|
788
|
+
payload as Record<string, unknown>,
|
|
789
|
+
combinedSettings,
|
|
790
|
+
undefined,
|
|
791
|
+
reviewFolder,
|
|
792
|
+
currentIteration,
|
|
793
|
+
);
|
|
794
|
+
logInfo(HOOK, `Saved review: ${reviewFile}`);
|
|
795
|
+
|
|
796
|
+
// Save plan snapshot for diffing between iterations
|
|
797
|
+
try {
|
|
798
|
+
fs.writeFileSync(path.join(reviewFolder, "plan.md"), plan, "utf-8");
|
|
799
|
+
logDebug(HOOK, `Saved plan snapshot: ${path.join(reviewFolder, "plan.md")}`);
|
|
800
|
+
} catch (e) {
|
|
801
|
+
logWarn(HOOK, `Failed to save plan snapshot: ${e}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Build inline summary with top issues (always emitted, even on pass)
|
|
805
|
+
const inlineSummary = buildInlineReviewSummary(combinedResult);
|
|
806
|
+
const topIssuesList = extractTopIssuesForTracker(combinedResult, 5);
|
|
807
|
+
const contextParts = [inlineSummary];
|
|
808
|
+
if (topIssuesList.length > 0) {
|
|
809
|
+
contextParts.push(`\nTop high-severity issues:\n${topIssuesList.map(i => `- ${i}`).join("\n")}`);
|
|
810
|
+
}
|
|
811
|
+
contextParts.push(`\nFull review: \`${reviewFile}\`\n`);
|
|
812
|
+
|
|
813
|
+
// Review decision
|
|
814
|
+
const { should_deny: shouldDeny, reason: denyReason, score: reviewScore } = computeReviewDecision(allVerdicts);
|
|
815
|
+
|
|
816
|
+
logInfo(HOOK, `REVIEW_DECISION: verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}, score=${reviewScore.toFixed(2)}`);
|
|
817
|
+
logDiagnostic(HOOK, "result", `verdict=${combinedResult.overall_verdict}, deny=${shouldDeny}, reason=${denyReason}`, {
|
|
818
|
+
decision: shouldDeny ? "deny" : "allow",
|
|
819
|
+
reasoning: `reason=${denyReason}, score=${reviewScore.toFixed(2)}`,
|
|
820
|
+
inputs: {
|
|
821
|
+
overall_verdict: combinedResult.overall_verdict,
|
|
822
|
+
review_score: Math.round(reviewScore * 100) / 100,
|
|
823
|
+
cli_count: Object.keys(cliResults).length,
|
|
824
|
+
agent_count: Object.keys(agentResults).length,
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Terminal progress
|
|
829
|
+
const verdictEmoji = shouldDeny ? "❌" : "✅";
|
|
830
|
+
eprint(`[plan-review] ${verdictEmoji} ${combinedResult.overall_verdict.toUpperCase()} (score=${reviewScore.toFixed(2)})`);
|
|
831
|
+
if (shouldDeny) {
|
|
832
|
+
eprint(`[plan-review] Blocking ExitPlanMode — ${denyReason}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Iteration logic:
|
|
836
|
+
// - On FAIL at max: extend max by 1 (grant one more revision chance)
|
|
837
|
+
// - On WARN: block but do NOT extend max (warns don't earn extra iterations)
|
|
838
|
+
// - On PASS: jump current to max so next call triggers early exit (no more reviews)
|
|
839
|
+
const isFail = overall === "fail";
|
|
840
|
+
if (iterationState && reviewsDir) {
|
|
841
|
+
iterationState.history.push({ hash: planHash, verdict: overall, timestamp: new Date().toISOString() });
|
|
842
|
+
|
|
843
|
+
if (isFail && iterationState.current >= iterationState.max) {
|
|
844
|
+
iterationState.max += 1;
|
|
845
|
+
logInfo(HOOK, `Extending max iterations to ${iterationState.max} due to fail at boundary (${iterationState.current}/${iterationState.max})`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (!shouldDeny) {
|
|
849
|
+
// Pass: set current to max so next call (current+1 > max) triggers early exit
|
|
850
|
+
iterationState.current = iterationState.max;
|
|
851
|
+
logInfo(HOOK, `Pass: setting current to max (${iterationState.max}) to exhaust iterations`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Merge newly graduated agents into persistent state
|
|
855
|
+
if (newlyGraduated.length > 0) {
|
|
856
|
+
const allGraduated = new Set([
|
|
857
|
+
...(iterationState.graduated ?? []),
|
|
858
|
+
...newlyGraduated,
|
|
859
|
+
]);
|
|
860
|
+
iterationState.graduated = [...allGraduated];
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
iterationState.current += 1;
|
|
864
|
+
saveIterationState(reviewsDir, iterationState);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Write review tracker (human-readable lifecycle summary)
|
|
868
|
+
const ccNativeReviewsDir = path.dirname(reviewFolder);
|
|
869
|
+
const trackerDecision = shouldDeny ? "blocked" : "allow";
|
|
870
|
+
const trackerEntry: ReviewTrackerEntry = {
|
|
871
|
+
iteration: currentIteration,
|
|
872
|
+
timestamp: new Date().toISOString().replace("T", " ").slice(0, 16),
|
|
873
|
+
planHash,
|
|
874
|
+
verdict: combinedResult.overall_verdict,
|
|
875
|
+
decision: trackerDecision,
|
|
876
|
+
score: reviewScore,
|
|
877
|
+
topIssues: extractTopIssuesForTracker(combinedResult, 5),
|
|
878
|
+
reviewFolder,
|
|
879
|
+
};
|
|
880
|
+
writeReviewTracker(ccNativeReviewsDir, trackerEntry);
|
|
881
|
+
logInfo(HOOK, `Updated review tracker: ${path.join(ccNativeReviewsDir, "review-tracker.md")}`);
|
|
882
|
+
|
|
883
|
+
// Emit output — always emit context with top issues + link; block only on fail
|
|
884
|
+
const contextText = contextParts.join("");
|
|
885
|
+
|
|
886
|
+
logDebug(HOOK, `REVIEW_CONTEXT_INJECTED: chars=${contextText.length}, inline_chars=${inlineSummary.length}`);
|
|
887
|
+
|
|
888
|
+
const REVIEWER_CAVEAT = "Reviewers have limited context compared to your full session — use your judgment to adopt valid points and dismiss genuine false positives. However, treat false positives as a clarity signal: if a reviewer misunderstood your plan, an agent executing it will likely hit the same confusion. Revise those sections to be unambiguous so no future reader — human or AI — makes the same mistake.";
|
|
889
|
+
const RESUBMIT_INSTRUCTION = "IMPORTANT: After revising the plan file, you MUST call ExitPlanMode again to trigger re-review. Do not end your turn or ask the user without calling ExitPlanMode.";
|
|
890
|
+
|
|
891
|
+
if (shouldDeny) {
|
|
892
|
+
const disposition = iterationState
|
|
893
|
+
? `hook_deny_iter_${iterationState.current - 1}`
|
|
894
|
+
: "hook_deny";
|
|
895
|
+
markPlanReviewed(sessionId, planHash, base, HOOK, iterationState ?? undefined, disposition);
|
|
896
|
+
const topIssuesText = extractTopIssuesText(combinedResult, 3, "high");
|
|
897
|
+
const highIssuesDoc = buildHighIssuesDocument(combinedResult);
|
|
898
|
+
const highIssuesPath = path.join(reviewFolder, "high-issues.md");
|
|
899
|
+
fs.writeFileSync(highIssuesPath, highIssuesDoc, "utf-8");
|
|
900
|
+
|
|
901
|
+
const iterInfo = iterationState
|
|
902
|
+
? ` (iteration ${iterationState.current - 1}/${iterationState.max}, score=${reviewScore.toFixed(2)})`
|
|
903
|
+
: ` (score=${reviewScore.toFixed(2)})`;
|
|
904
|
+
|
|
905
|
+
emitContextAndBlock(
|
|
906
|
+
contextText,
|
|
907
|
+
`Plan review FAILED${iterInfo}. ` +
|
|
908
|
+
`Critical issues: ${topIssuesText}. ` +
|
|
909
|
+
`IMPORTANT: Read \`${highIssuesPath}\` for ALL high-severity issues — ` +
|
|
910
|
+
`this file contains only the most critical findings, no noise. ` +
|
|
911
|
+
`${REVIEWER_CAVEAT} ` +
|
|
912
|
+
`Revise the plan to address these issues, then call ExitPlanMode again. ` +
|
|
913
|
+
RESUBMIT_INSTRUCTION,
|
|
914
|
+
);
|
|
915
|
+
} else {
|
|
916
|
+
markPlanReviewed(sessionId, planHash, base, HOOK, iterationState ?? undefined, "allow");
|
|
917
|
+
emitContext(contextText);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
runHookAsync(main, "cc_native_plan_review");
|