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.
Files changed (249) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.d.ts +11 -6
  3. package/dist/commands/clear.js +229 -381
  4. package/dist/commands/init/index.d.ts +1 -17
  5. package/dist/commands/init/index.js +22 -107
  6. package/dist/lib/gitignore-manager.d.ts +32 -0
  7. package/dist/lib/gitignore-manager.js +141 -2
  8. package/dist/lib/template-installer.d.ts +7 -12
  9. package/dist/lib/template-installer.js +69 -193
  10. package/dist/lib/template-settings-reconstructor.d.ts +35 -0
  11. package/dist/lib/template-settings-reconstructor.js +130 -0
  12. package/dist/templates/CLAUDE.md +8 -8
  13. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  14. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  15. package/dist/templates/_shared/.claude/settings.json +7 -7
  16. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  17. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  18. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  19. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  20. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  21. package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
  22. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  23. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  24. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  25. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  26. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  27. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +138 -0
  28. package/dist/templates/_shared/lib-ts/base/constants.ts +306 -0
  29. package/dist/templates/_shared/lib-ts/base/git-state.ts +58 -0
  30. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +439 -0
  31. package/dist/templates/_shared/lib-ts/base/inference.ts +252 -0
  32. package/dist/templates/_shared/lib-ts/base/logger.ts +250 -0
  33. package/dist/templates/_shared/lib-ts/base/state-io.ts +116 -0
  34. package/dist/templates/_shared/lib-ts/base/stop-words.ts +184 -0
  35. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +162 -0
  36. package/dist/templates/_shared/lib-ts/base/utils.ts +184 -0
  37. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +438 -0
  38. package/dist/templates/_shared/lib-ts/context/context-selector.ts +515 -0
  39. package/dist/templates/_shared/lib-ts/context/context-store.ts +707 -0
  40. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +316 -0
  41. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +185 -0
  42. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +216 -0
  43. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  44. package/dist/templates/_shared/lib-ts/package.json +21 -0
  45. package/dist/templates/_shared/lib-ts/templates/formatters.ts +104 -0
  46. package/dist/templates/_shared/{lib/templates/plan_context.py → lib-ts/templates/plan-context.ts} +14 -22
  47. package/dist/templates/_shared/lib-ts/tsconfig.json +13 -0
  48. package/dist/templates/_shared/lib-ts/types.ts +164 -0
  49. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  50. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  51. package/dist/templates/_shared/scripts/save_handoff.ts +359 -0
  52. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  53. package/dist/templates/cc-native/.claude/settings.json +175 -185
  54. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  55. package/dist/templates/cc-native/_cc-native/agents/ARCH-EVOLUTION.md +63 -0
  56. package/dist/templates/cc-native/_cc-native/agents/ARCH-PATTERNS.md +62 -0
  57. package/dist/templates/cc-native/_cc-native/agents/ARCH-STRUCTURE.md +63 -0
  58. package/dist/templates/cc-native/_cc-native/agents/{ASSUMPTION-CHAIN-TRACER.md → ASSUMPTION-TRACER.md} +6 -10
  59. package/dist/templates/cc-native/_cc-native/agents/CLARITY-AUDITOR.md +6 -10
  60. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +74 -3
  61. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-FEASIBILITY.md +67 -0
  62. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-GAPS.md +71 -0
  63. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-ORDERING.md +63 -0
  64. package/dist/templates/cc-native/_cc-native/agents/CONSTRAINT-VALIDATOR.md +73 -0
  65. package/dist/templates/cc-native/_cc-native/agents/DESIGN-ADR-VALIDATOR.md +62 -0
  66. package/dist/templates/cc-native/_cc-native/agents/DESIGN-SCALE-MATCHER.md +65 -0
  67. package/dist/templates/cc-native/_cc-native/agents/DEVILS-ADVOCATE.md +6 -9
  68. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-PHILOSOPHY.md +87 -0
  69. package/dist/templates/cc-native/_cc-native/agents/HANDOFF-READINESS.md +5 -9
  70. package/dist/templates/cc-native/_cc-native/agents/{HIDDEN-COMPLEXITY-DETECTOR.md → HIDDEN-COMPLEXITY.md} +6 -10
  71. package/dist/templates/cc-native/_cc-native/agents/INCREMENTAL-DELIVERY.md +67 -0
  72. package/dist/templates/cc-native/_cc-native/agents/PLAN-ORCHESTRATOR.md +91 -18
  73. package/dist/templates/cc-native/_cc-native/agents/RISK-DEPENDENCY.md +63 -0
  74. package/dist/templates/cc-native/_cc-native/agents/RISK-FMEA.md +67 -0
  75. package/dist/templates/cc-native/_cc-native/agents/RISK-PREMORTEM.md +72 -0
  76. package/dist/templates/cc-native/_cc-native/agents/RISK-REVERSIBILITY.md +75 -0
  77. package/dist/templates/cc-native/_cc-native/agents/SCOPE-BOUNDARY.md +78 -0
  78. package/dist/templates/cc-native/_cc-native/agents/SIMPLICITY-GUARDIAN.md +5 -9
  79. package/dist/templates/cc-native/_cc-native/agents/SKEPTIC.md +16 -12
  80. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-BEHAVIOR-AUDITOR.md +62 -0
  81. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-CHARACTERIZATION.md +72 -0
  82. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-FIRST-VALIDATOR.md +62 -0
  83. package/dist/templates/cc-native/_cc-native/agents/TESTDRIVEN-PYRAMID-ANALYZER.md +62 -0
  84. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-COSTS.md +68 -0
  85. package/dist/templates/cc-native/_cc-native/agents/TRADEOFF-STAKEHOLDERS.md +66 -0
  86. package/dist/templates/cc-native/_cc-native/agents/VERIFY-COVERAGE.md +75 -0
  87. package/dist/templates/cc-native/_cc-native/agents/VERIFY-STRENGTH.md +70 -0
  88. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  89. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  90. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  91. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  92. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  93. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  94. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  95. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  96. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  97. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  98. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  99. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  100. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  101. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  102. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  103. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  104. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  105. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  106. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  107. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  108. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  109. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  110. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  111. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  112. package/dist/templates/cc-native/_cc-native/plan-review.config.json +12 -16
  113. package/oclif.manifest.json +1 -1
  114. package/package.json +1 -1
  115. package/dist/lib/template-merger.d.ts +0 -47
  116. package/dist/lib/template-merger.js +0 -162
  117. package/dist/templates/_shared/hooks/__init__.py +0 -16
  118. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  130. package/dist/templates/_shared/hooks/archive_plan.py +0 -169
  131. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  132. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  133. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  134. package/dist/templates/_shared/hooks/session_end.py +0 -173
  135. package/dist/templates/_shared/hooks/session_start.py +0 -206
  136. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  137. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  138. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  139. package/dist/templates/_shared/lib/__init__.py +0 -1
  140. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  142. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  145. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  146. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  149. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  150. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  151. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  152. package/dist/templates/_shared/lib/base/constants.py +0 -358
  153. package/dist/templates/_shared/lib/base/hook_utils.py +0 -341
  154. package/dist/templates/_shared/lib/base/inference.py +0 -318
  155. package/dist/templates/_shared/lib/base/logger.py +0 -291
  156. package/dist/templates/_shared/lib/base/stop_words.py +0 -213
  157. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  158. package/dist/templates/_shared/lib/base/utils.py +0 -242
  159. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  160. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  161. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  162. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  163. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  164. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  165. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  166. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  167. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  168. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  169. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  170. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  171. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  172. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  173. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  174. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  175. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  176. package/dist/templates/_shared/lib/context/plan_manager.py +0 -204
  177. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  178. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  179. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  180. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  181. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  182. package/dist/templates/_shared/lib/templates/README.md +0 -206
  183. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  184. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  185. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  186. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  187. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  188. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  189. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  190. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  191. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  192. package/dist/templates/_shared/scripts/status_line.py +0 -701
  193. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  194. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  195. package/dist/templates/cc-native/MIGRATION.md +0 -86
  196. package/dist/templates/cc-native/_cc-native/agents/ACCESSIBILITY-TESTER.md +0 -79
  197. package/dist/templates/cc-native/_cc-native/agents/ARCHITECT-REVIEWER.md +0 -48
  198. package/dist/templates/cc-native/_cc-native/agents/CODE-REVIEWER.md +0 -70
  199. package/dist/templates/cc-native/_cc-native/agents/COMPLETENESS-CHECKER.md +0 -59
  200. package/dist/templates/cc-native/_cc-native/agents/CONTEXT-EXTRACTOR.md +0 -92
  201. package/dist/templates/cc-native/_cc-native/agents/DOCUMENTATION-REVIEWER.md +0 -51
  202. package/dist/templates/cc-native/_cc-native/agents/FEASIBILITY-ANALYST.md +0 -57
  203. package/dist/templates/cc-native/_cc-native/agents/FRESH-PERSPECTIVE.md +0 -54
  204. package/dist/templates/cc-native/_cc-native/agents/INCENTIVE-MAPPER.md +0 -61
  205. package/dist/templates/cc-native/_cc-native/agents/PENETRATION-TESTER.md +0 -79
  206. package/dist/templates/cc-native/_cc-native/agents/PERFORMANCE-ENGINEER.md +0 -75
  207. package/dist/templates/cc-native/_cc-native/agents/PRECEDENT-FINDER.md +0 -70
  208. package/dist/templates/cc-native/_cc-native/agents/REVERSIBILITY-ANALYST.md +0 -61
  209. package/dist/templates/cc-native/_cc-native/agents/RISK-ASSESSOR.md +0 -58
  210. package/dist/templates/cc-native/_cc-native/agents/SECOND-ORDER-ANALYST.md +0 -61
  211. package/dist/templates/cc-native/_cc-native/agents/STAKEHOLDER-ADVOCATE.md +0 -55
  212. package/dist/templates/cc-native/_cc-native/agents/TRADE-OFF-ILLUMINATOR.md +0 -204
  213. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  214. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  215. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  216. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  217. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  218. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  219. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  220. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -869
  221. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  222. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  223. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  224. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  225. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  226. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  227. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  228. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  229. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  230. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  231. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  232. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  233. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  234. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  235. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  236. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  237. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  238. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  239. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  240. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  241. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  242. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  243. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  244. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  245. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  246. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1027
  247. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  248. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  249. 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");