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
@@ -1,1027 +0,0 @@
1
- """
2
- CC-Native shared utilities.
3
-
4
- Provides common functions used across all cc-native hooks:
5
- - Core utilities (eprint, now_local, project_dir, sanitize_filename)
6
- - Plan hash deduplication (compute_plan_hash, get_review_marker_path, etc.)
7
- - JSON parsing (parse_json_maybe, coerce_to_review, worst_verdict)
8
- - Artifact writing (format_markdown, write_artifacts, find_plan_file)
9
- - Constants (REVIEW_SCHEMA, DEFAULT_DISPLAY)
10
- - Dataclasses (ReviewerResult)
11
- """
12
-
13
- import hashlib
14
- import json
15
- import os
16
- import re
17
- import sys
18
- import tempfile
19
- from dataclasses import dataclass
20
- from datetime import datetime
21
- from pathlib import Path
22
- from typing import Any, Dict, List, Optional, Tuple
23
-
24
- try:
25
- from .constants import ENABLE_ROBUST_PLAN_WRITES
26
- except ImportError:
27
- # When imported directly via sys.path (not as a package)
28
- from constants import ENABLE_ROBUST_PLAN_WRITES
29
-
30
- # Import atomic_write from shared lib (canonical copy)
31
- try:
32
- from ...lib.base.atomic_write import atomic_write
33
- except ImportError:
34
- # Fallback for direct execution
35
- _shared_lib = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib"
36
- import importlib.util
37
- _spec = importlib.util.spec_from_file_location(
38
- "atomic_write", str(_shared_lib / "base" / "atomic_write.py")
39
- )
40
- _mod = importlib.util.module_from_spec(_spec)
41
- _spec.loader.exec_module(_mod)
42
- atomic_write = _mod.atomic_write
43
-
44
- # Import canonical utilities from shared lib (with Windows bug fixes)
45
- try:
46
- from ...lib.base.utils import (
47
- eprint,
48
- now_local,
49
- project_dir,
50
- sanitize_filename,
51
- sanitize_title,
52
- )
53
- from ...lib.base.logger import log_debug, log_info, log_warn, log_error
54
- except ImportError:
55
- # Fallback for direct execution
56
- import sys
57
- from pathlib import Path
58
- _shared_lib = Path(__file__).resolve().parent.parent.parent / "_shared" / "lib"
59
- sys.path.insert(0, str(_shared_lib))
60
- from base.utils import (
61
- eprint,
62
- now_local,
63
- project_dir,
64
- sanitize_filename,
65
- sanitize_title,
66
- )
67
- from base.logger import log_debug, log_info, log_warn, log_error
68
-
69
-
70
- # ---------------------------
71
- # Constants
72
- # ---------------------------
73
-
74
- DEFAULT_DISPLAY: Dict[str, int] = {
75
- "maxIssues": 12,
76
- "maxMissingSections": 12,
77
- "maxQuestions": 12,
78
- }
79
-
80
- DEFAULT_SANITIZATION: Dict[str, int] = {
81
- "maxSessionIdLength": 32,
82
- "maxTitleLength": 50,
83
- }
84
-
85
- REVIEW_SCHEMA: Dict[str, Any] = {
86
- "type": "object",
87
- "properties": {
88
- "verdict": {"type": "string", "enum": ["pass", "warn", "fail"]},
89
- "summary": {"type": "string", "minLength": 20},
90
- "issues": {
91
- "type": "array",
92
- "items": {
93
- "type": "object",
94
- "properties": {
95
- "severity": {"type": "string", "enum": ["high", "medium", "low"]},
96
- "category": {"type": "string"},
97
- "issue": {"type": "string"},
98
- "suggested_fix": {"type": "string"},
99
- },
100
- "required": ["severity", "category", "issue", "suggested_fix"],
101
- "additionalProperties": False,
102
- },
103
- },
104
- "missing_sections": {"type": "array", "items": {"type": "string"}},
105
- "questions": {"type": "array", "items": {"type": "string"}},
106
- },
107
- "required": ["verdict", "summary", "issues", "missing_sections", "questions"],
108
- "additionalProperties": False,
109
- }
110
-
111
-
112
- # ---------------------------
113
- # Dataclasses
114
- # ---------------------------
115
-
116
- @dataclass
117
- class ReviewerResult:
118
- """Result from a plan reviewer (Codex, Gemini, or Claude agent)."""
119
- name: str
120
- ok: bool
121
- verdict: str # pass|warn|fail|error|skip
122
- data: Dict[str, Any]
123
- raw: str
124
- err: str
125
-
126
-
127
- # ---------------------------
128
- # Plan hash deduplication
129
- # ---------------------------
130
-
131
- def compute_plan_hash(plan_content: str) -> str:
132
- """Compute a hash of the plan content."""
133
- return hashlib.sha256(plan_content.encode("utf-8")).hexdigest()[:16]
134
-
135
-
136
- def get_review_marker_path(session_id: str) -> Path:
137
- """Get path to review marker file for this session."""
138
- safe_id = re.sub(r'[^a-zA-Z0-9_-]', '_', session_id)[:32]
139
- return Path(tempfile.gettempdir()) / f"cc-native-plan-reviewed-{safe_id}.json"
140
-
141
-
142
- def is_plan_already_reviewed(session_id: str, plan_hash: str) -> bool:
143
- """Check if this exact plan has already been reviewed in this session."""
144
- marker_path = get_review_marker_path(session_id)
145
- if not marker_path.exists():
146
- return False
147
- try:
148
- data = json.loads(marker_path.read_text(encoding="utf-8"))
149
- stored_hash = data.get("plan_hash", "")
150
- return stored_hash == plan_hash
151
- except Exception:
152
- return False
153
-
154
-
155
- def was_plan_previously_denied(session_id: str, plan_hash: str) -> bool:
156
- """Check if this plan hash was previously reviewed and denied."""
157
- marker_path = get_review_marker_path(session_id)
158
- if not marker_path.exists():
159
- return False
160
- try:
161
- data = json.loads(marker_path.read_text(encoding="utf-8"))
162
- return data.get("plan_hash") == plan_hash and data.get("decision") == "deny"
163
- except Exception:
164
- return False
165
-
166
-
167
- def mark_plan_reviewed(
168
- session_id: str,
169
- plan_hash: str,
170
- hook_name: str = "cc-native",
171
- iteration_state: Optional[Dict[str, Any]] = None,
172
- decision: str = "allow",
173
- ) -> None:
174
- """Mark this plan as reviewed (stores hash and decision in marker file).
175
-
176
- Args:
177
- session_id: The session identifier
178
- plan_hash: Hash of the plan content
179
- hook_name: Name of the hook (for logging)
180
- iteration_state: Optional iteration state dict with current, max, verdict info
181
- decision: Review decision - "allow" or "deny"
182
- """
183
- marker = get_review_marker_path(session_id)
184
- try:
185
- data: Dict[str, Any] = {
186
- "plan_hash": plan_hash,
187
- "reviewed_at": datetime.now().isoformat(),
188
- "decision": decision,
189
- }
190
-
191
- # Include iteration info if provided
192
- if iteration_state:
193
- data["iteration"] = {
194
- "current": iteration_state.get("current", 1),
195
- "max": iteration_state.get("max", 1),
196
- "complexity": iteration_state.get("complexity", "unknown"),
197
- }
198
- # Include latest verdict from history if available
199
- history = iteration_state.get("history", [])
200
- if history:
201
- data["iteration"]["latest_verdict"] = history[-1].get("verdict", "unknown")
202
-
203
- marker.write_text(json.dumps(data), encoding="utf-8")
204
- iter_info = f" (iteration {data.get('iteration', {}).get('current', '?')}/{data.get('iteration', {}).get('max', '?')})" if iteration_state else ""
205
- log_info(hook_name, f"Created review marker: {marker} (hash: {plan_hash}){iter_info}")
206
- except Exception as e:
207
- log_warn(hook_name, f"Failed to create review marker: {e}")
208
-
209
-
210
- # ---------------------------
211
- # Questions asked state
212
- # ---------------------------
213
-
214
- def get_questions_asked_marker_path(session_id: str) -> Path:
215
- """Get path to questions-asked marker file for this session."""
216
- safe_id = re.sub(r'[^a-zA-Z0-9_-]', '_', session_id)[:32]
217
- return Path(tempfile.gettempdir()) / f"cc-native-questions-asked-{safe_id}.json"
218
-
219
-
220
- def was_questions_asked(session_id: str) -> bool:
221
- """Check if AskUserQuestion was called this session.
222
-
223
- Returns False on any error (fail-safe: allow feature to work).
224
- """
225
- try:
226
- return get_questions_asked_marker_path(session_id).exists()
227
- except Exception:
228
- return False
229
-
230
-
231
- def mark_questions_asked(session_id: str) -> bool:
232
- """Mark that AskUserQuestion was called. Returns True on success.
233
-
234
- Only stores timestamp, no user data. Returns False on error.
235
- """
236
- try:
237
- marker = get_questions_asked_marker_path(session_id)
238
- marker.write_text(json.dumps({"asked_at": datetime.now().isoformat()}), encoding="utf-8")
239
- return True
240
- except Exception as e:
241
- log_warn("utils", f"Failed to write questions-asked marker: {e}")
242
- return False
243
-
244
-
245
- # ---------------------------
246
- # JSON parsing
247
- # ---------------------------
248
-
249
- def parse_json_maybe(text: str, require_fields: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
250
- """Try strict JSON parse. If that fails, attempt to extract the first {...} block.
251
-
252
- Args:
253
- text: Raw text that may contain JSON
254
- require_fields: Optional list of field names to check for in parsed result.
255
- If provided and fields are missing, a warning is logged but
256
- the object is still returned.
257
-
258
- Returns:
259
- Parsed dict or None if parsing failed entirely.
260
- """
261
- text = text.strip()
262
- if not text:
263
- return None
264
-
265
- obj: Optional[Dict[str, Any]] = None
266
- parse_method = None
267
-
268
- try:
269
- parsed = json.loads(text)
270
- if isinstance(parsed, dict):
271
- obj = parsed
272
- parse_method = "strict"
273
- except Exception:
274
- pass
275
-
276
- # Heuristic: try to extract a JSON object substring
277
- if obj is None:
278
- start = text.find("{")
279
- end = text.rfind("}")
280
- if start != -1 and end != -1 and end > start:
281
- candidate = text[start : end + 1]
282
- try:
283
- parsed = json.loads(candidate)
284
- if isinstance(parsed, dict):
285
- obj = parsed
286
- parse_method = "heuristic"
287
- log_debug("parse", f"Used heuristic extraction (chars {start}-{end})")
288
- except Exception:
289
- log_debug("parse", f"Heuristic extraction failed for candidate at chars {start}-{end}")
290
- return None
291
-
292
- # If we parsed something, validate required fields
293
- if obj and require_fields:
294
- missing = [f for f in require_fields if f not in obj or not obj[f]]
295
- if missing:
296
- log_warn("parse", f"Parsed JSON ({parse_method}) missing/empty fields: {missing}")
297
- log_debug("parse", f"Keys present: {list(obj.keys())}")
298
-
299
- return obj
300
-
301
-
302
- def coerce_to_review(obj: Optional[Dict[str, Any]], default_fix_msg: str = "Retry or check configuration.") -> Tuple[bool, str, Dict[str, Any]]:
303
- """Validate/normalize to our expected structure.
304
-
305
- Returns:
306
- Tuple of (ok, verdict, normalized_data).
307
- normalized_data includes 'summary_source' field: 'reviewer' if summary was provided,
308
- 'default' if it was defaulted due to missing/empty summary.
309
- """
310
- if not obj:
311
- log_warn("coerce", "No object provided to coerce_to_review")
312
- return False, "error", {
313
- "verdict": "fail",
314
- "summary": "No structured output returned.",
315
- "summary_source": "default",
316
- "issues": [{"severity": "high", "category": "tooling", "issue": "Reviewer returned no JSON.", "suggested_fix": default_fix_msg}],
317
- "missing_sections": [],
318
- "questions": [],
319
- }
320
-
321
- verdict = obj.get("verdict")
322
- if verdict not in ("pass", "warn", "fail"):
323
- log_warn("coerce", f"Invalid or missing verdict '{verdict}', defaulting to 'warn'")
324
- verdict = "warn"
325
-
326
- # Log when fields are being defaulted
327
- summary_raw = str(obj.get("summary", "")).strip()
328
- if not summary_raw:
329
- log_warn("coerce", "summary missing or empty from parsed output, using default")
330
- # Add diagnostic output
331
- log_debug("coerce", f"Raw object keys: {list(obj.keys()) if obj else 'None'}")
332
- if obj:
333
- log_debug("coerce", f"verdict={obj.get('verdict')}, issues_count={len(obj.get('issues', []))}")
334
- if not obj.get("issues"):
335
- log_debug("coerce", "issues array empty or missing")
336
-
337
- norm = {
338
- "verdict": verdict,
339
- "summary": summary_raw or "No summary provided.",
340
- "summary_source": "reviewer" if summary_raw else "default",
341
- "issues": obj.get("issues") if isinstance(obj.get("issues"), list) else [],
342
- "missing_sections": obj.get("missing_sections") if isinstance(obj.get("missing_sections"), list) else [],
343
- "questions": obj.get("questions") if isinstance(obj.get("questions"), list) else [],
344
- }
345
-
346
- return True, verdict, norm
347
-
348
-
349
- def worst_verdict(verdicts: List[str]) -> str:
350
- """Return the worst verdict from a list."""
351
- order = {"pass": 0, "warn": 1, "fail": 2, "skip": 0, "error": 1}
352
- worst = "pass"
353
- for v in verdicts:
354
- if order.get(v, 1) > order.get(worst, 0):
355
- worst = v
356
- if worst == "error":
357
- return "warn"
358
- return worst
359
-
360
-
361
- def compute_review_decision(
362
- all_verdicts: List[str],
363
- warn_threshold: float = 0.5,
364
- ) -> Tuple[bool, str, float]:
365
- """Verdict aggregation: only fail triggers a block.
366
-
367
- Fail Veto: Any fail -> deny. From safety engineering (ISO 61508) —
368
- critical alarms use zero-tolerance.
369
-
370
- Warns are informational only — the warn_ratio is computed for logging
371
- and visibility but does NOT trigger blocking.
372
-
373
- Error exclusion: Detectors that produce no signal (error/skip) are excluded
374
- from the denominator. They provide no information about plan quality.
375
-
376
- Args:
377
- all_verdicts: List of verdict strings from all reviewers.
378
- warn_threshold: Kept for backward compatibility. No longer used for blocking.
379
-
380
- Returns:
381
- Tuple of (should_deny, reason, score).
382
- - should_deny: True if the plan should be denied.
383
- - reason: "fail_veto", "acceptable", or "no_signal".
384
- - score: 1.0 for fail_veto, warn_ratio for informational cases, 0.0 for no_signal.
385
- """
386
- # Exclude non-signal verdicts
387
- signal_verdicts = [v for v in all_verdicts if v in ("pass", "warn", "fail")]
388
-
389
- if not signal_verdicts:
390
- return False, "no_signal", 0.0
391
-
392
- # Only fail blocks — warns are informational
393
- fail_count = signal_verdicts.count("fail")
394
- if fail_count > 0:
395
- return True, "fail_veto", 1.0
396
-
397
- # Warn ratio still computed for logging/visibility, but does NOT block
398
- warn_count = signal_verdicts.count("warn")
399
- warn_ratio = warn_count / len(signal_verdicts)
400
- return False, "acceptable", warn_ratio
401
-
402
-
403
- # ---------------------------
404
- # Artifact writing
405
- # ---------------------------
406
-
407
- def find_plan_file() -> Optional[str]:
408
- """Find the most recent plan file in ~/.claude/plans/."""
409
- plans_dir = Path.home() / ".claude" / "plans"
410
- if not plans_dir.exists():
411
- return None
412
- plan_files = list(plans_dir.glob("*.md"))
413
- if not plan_files:
414
- return None
415
- plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
416
- return str(plan_files[0])
417
-
418
-
419
- def get_state_path_from_plan(plan_path: str) -> Path:
420
- """Derive state file path from plan file path.
421
-
422
- The state file is stored adjacent to the plan file with a .state.json extension.
423
- This prevents state loss when session IDs change or temp files are cleaned up.
424
-
425
- Example: ~/.claude/plans/foo.md -> ~/.claude/plans/foo.state.json
426
- """
427
- plan_file = Path(plan_path)
428
- return plan_file.with_suffix('.state.json')
429
-
430
-
431
- def format_review_markdown(
432
- results: List[ReviewerResult],
433
- overall: str,
434
- title: str = "CC-Native Plan Review",
435
- settings: Optional[Dict[str, Any]] = None,
436
- ) -> str:
437
- """Format review results as markdown."""
438
- display = DEFAULT_DISPLAY.copy()
439
- if settings:
440
- display = settings.get("display", DEFAULT_DISPLAY)
441
-
442
- max_issues = display.get("maxIssues", 12)
443
- max_missing = display.get("maxMissingSections", 12)
444
- max_questions = display.get("maxQuestions", 12)
445
-
446
- lines: List[str] = []
447
- lines.append(f"# {title}\n")
448
- lines.append(f"**Overall verdict:** `{overall.upper()}`\n")
449
-
450
- for r in results:
451
- lines.append(f"## {r.name.title() if r.name.islower() else r.name}\n")
452
- lines.append(f"- ok: `{r.ok}`")
453
- lines.append(f"- verdict: `{r.verdict}`")
454
- if r.data:
455
- summary = r.data.get('summary', '').strip()
456
- if r.data.get('summary_source') == 'default':
457
- lines.append(f"- summary: ⚠️ {summary} *(reviewer did not return summary)*")
458
- else:
459
- lines.append(f"- summary: {summary}")
460
- issues = r.data.get("issues", [])
461
- if issues:
462
- lines.append("\n### Issues")
463
- for it in issues[:max_issues]:
464
- sev = it.get("severity", "medium")
465
- cat = it.get("category", "general")
466
- issue = it.get("issue", "")
467
- fix = it.get("suggested_fix", "")
468
- lines.append(f"- **[{sev}] {cat}**: {issue}\n - fix: {fix}")
469
- missing = r.data.get("missing_sections", [])
470
- if missing:
471
- lines.append("\n### Missing Sections")
472
- for m in missing[:max_missing]:
473
- lines.append(f"- {m}")
474
- qs = r.data.get("questions", [])
475
- if qs:
476
- lines.append("\n### Questions")
477
- for q in qs[:max_questions]:
478
- lines.append(f"- {q}")
479
- else:
480
- lines.append(f"- note: {r.err or 'no structured output'}")
481
- lines.append("")
482
-
483
- return "\n".join(lines).strip() + "\n"
484
-
485
-
486
- def write_review_artifacts(
487
- base: Path,
488
- plan: str,
489
- md: str,
490
- results: List[ReviewerResult],
491
- payload: Dict[str, Any],
492
- subdir: str = "reviews",
493
- ) -> Path:
494
- """Write review artifacts to _output/cc-native/plans/{subdir}/."""
495
- ts = now_local()
496
- date_folder = ts.strftime("%Y-%m-%d")
497
- time_part = ts.strftime("%H%M%S")
498
- sid = sanitize_filename(str(payload.get("session_id", "unknown")))
499
-
500
- out_dir = base / "_output" / "cc-native" / "plans" / subdir / date_folder
501
- out_dir.mkdir(parents=True, exist_ok=True)
502
-
503
- plan_path = out_dir / f"{time_part}-session-{sid}-plan.md"
504
- review_path = out_dir / f"{time_part}-session-{sid}-review.md"
505
-
506
- plan_path.write_text(plan, encoding="utf-8")
507
- review_path.write_text(md, encoding="utf-8")
508
-
509
- for r in results:
510
- if r.data:
511
- (out_dir / f"{time_part}-session-{sid}-{r.name}.json").write_text(
512
- json.dumps(r.data, indent=2, ensure_ascii=False),
513
- encoding="utf-8",
514
- )
515
-
516
- return review_path
517
-
518
-
519
- @dataclass
520
- class OrchestratorResult:
521
- """Result from the plan orchestrator."""
522
- complexity: str # simple | medium | high
523
- category: str # code | infrastructure | documentation | life | business | design | research
524
- selected_agents: List[str]
525
- reasoning: str
526
- skip_reason: Optional[str] = None
527
- error: Optional[str] = None
528
-
529
-
530
- @dataclass
531
- class CombinedReviewResult:
532
- """Combined result from all review phases."""
533
- plan_hash: str
534
- overall_verdict: str
535
- cli_reviewers: Dict[str, ReviewerResult]
536
- orchestration: Optional[OrchestratorResult]
537
- agents: Dict[str, ReviewerResult]
538
- timestamp: str
539
-
540
-
541
- def format_combined_markdown(
542
- result: CombinedReviewResult,
543
- settings: Optional[Dict[str, Any]] = None,
544
- ) -> str:
545
- """Format combined review result as a single markdown document."""
546
- display = DEFAULT_DISPLAY.copy()
547
- if settings:
548
- display = settings.get("display", DEFAULT_DISPLAY)
549
-
550
- max_issues = display.get("maxIssues", 12)
551
- max_missing = display.get("maxMissingSections", 12)
552
- max_questions = display.get("maxQuestions", 12)
553
-
554
- lines: List[str] = []
555
- lines.append("# CC-Native Plan Review\n")
556
- lines.append(f"**Overall Verdict:** `{result.overall_verdict.upper()}`")
557
- lines.append(f"**Plan Hash:** `{result.plan_hash}`\n")
558
- lines.append("---\n")
559
-
560
- # CLI Reviewers section
561
- if result.cli_reviewers:
562
- lines.append("## CLI Reviewers\n")
563
- for name, r in result.cli_reviewers.items():
564
- lines.append(f"### {name.title()}\n")
565
- lines.append(f"- verdict: `{r.verdict}`")
566
- if r.data:
567
- summary = r.data.get('summary', '').strip()
568
- if r.data.get('summary_source') == 'default':
569
- lines.append(f"- summary: ⚠️ {summary} *(reviewer did not return summary)*")
570
- else:
571
- lines.append(f"- summary: {summary}")
572
- _append_review_details(lines, r.data, max_issues, max_missing, max_questions)
573
- elif r.err:
574
- lines.append(f"- error: {r.err}")
575
- lines.append("")
576
-
577
- # Orchestration section
578
- if result.orchestration:
579
- lines.append("---\n")
580
- lines.append("## Orchestration\n")
581
- lines.append(f"- **Complexity:** `{result.orchestration.complexity}`")
582
- lines.append(f"- **Category:** `{result.orchestration.category}`")
583
- agents_str = ", ".join(result.orchestration.selected_agents) if result.orchestration.selected_agents else "None"
584
- lines.append(f"- **Agents Selected:** {agents_str}")
585
- lines.append(f"- **Reasoning:** {result.orchestration.reasoning}")
586
- if result.orchestration.skip_reason:
587
- lines.append(f"- **Skip Reason:** {result.orchestration.skip_reason}")
588
- if result.orchestration.error:
589
- lines.append(f"- **Error:** {result.orchestration.error}")
590
- lines.append("")
591
-
592
- # Agent Reviews section
593
- if result.agents:
594
- lines.append("---\n")
595
- lines.append("## Agent Reviews\n")
596
- for name, r in result.agents.items():
597
- lines.append(f"### {name}\n")
598
- lines.append(f"- verdict: `{r.verdict}`")
599
- if r.data:
600
- summary = r.data.get('summary', '').strip()
601
- if r.data.get('summary_source') == 'default':
602
- lines.append(f"- summary: ⚠️ {summary} *(reviewer did not return summary)*")
603
- else:
604
- lines.append(f"- summary: {summary}")
605
- _append_review_details(lines, r.data, max_issues, max_missing, max_questions)
606
- elif r.err:
607
- lines.append(f"- error: {r.err}")
608
- lines.append("")
609
-
610
- return "\n".join(lines).strip() + "\n"
611
-
612
-
613
- def build_inline_review_summary(
614
- combined: CombinedReviewResult,
615
- max_issues: int = 5,
616
- max_chars: int = 800,
617
- ) -> str:
618
- """Build compact inline summary of HIGH-severity review findings for additionalContext.
619
-
620
- Returns an overall verdict line plus up to 5 high-severity issues as bullet points.
621
- Per-reviewer verdicts, missing sections, and key questions are omitted from inline
622
- output (they remain in the full review artifact on disk).
623
-
624
- Args:
625
- combined: The combined review result from all reviewers.
626
- max_issues: Maximum number of high-severity issues to include.
627
- max_chars: Character budget for the summary (truncated if exceeded).
628
-
629
- Returns:
630
- Compact summary string, or empty string if no high-severity findings.
631
- """
632
- # Collect HIGH severity issues across all reviewers
633
- all_reviewers: List[ReviewerResult] = []
634
- all_reviewers.extend(combined.cli_reviewers.values())
635
- all_reviewers.extend(combined.agents.values())
636
-
637
- high_issues: List[Dict[str, Any]] = []
638
- for r in all_reviewers:
639
- if not r.data:
640
- continue
641
- for issue in r.data.get("issues", []):
642
- if issue.get("severity") == "high":
643
- high_issues.append({**issue, "_reviewer": r.name})
644
-
645
- parts: List[str] = []
646
-
647
- # Overall verdict line
648
- parts.append(f"**Plan Review: {combined.overall_verdict.upper()}**"
649
- + (f" ({len(high_issues)} high-severity issue{'s' if len(high_issues) != 1 else ''})"
650
- if high_issues else ""))
651
-
652
- # High-severity issue bullets (max 5)
653
- for issue in high_issues[:max_issues]:
654
- cat = issue.get("category", "general")
655
- text = issue.get("issue", "")
656
- fix = issue.get("suggested_fix", "")
657
- reviewer = issue.get("_reviewer", "unknown")
658
- line = f"- [{cat}] {text}"
659
- if fix:
660
- line += f" \u2192 {fix}"
661
- line += f" ({reviewer})"
662
- parts.append(line)
663
- remaining = len(high_issues) - max_issues
664
- if remaining > 0:
665
- parts.append(f" ...and {remaining} more")
666
-
667
- result = "\n".join(parts)
668
- if len(result) > max_chars:
669
- result = result[:max_chars - 3] + "..."
670
- return result
671
-
672
-
673
- def extract_top_issues_text(
674
- combined: CombinedReviewResult,
675
- max_count: int = 3,
676
- severity: str = "high",
677
- ) -> str:
678
- """Extract top issues as a compact text string for permissionDecisionReason.
679
-
680
- Args:
681
- combined: The combined review result.
682
- max_count: Maximum number of issues to include.
683
- severity: Severity level to filter for.
684
-
685
- Returns:
686
- Compact semicolon-separated issue text.
687
- """
688
- all_reviewers: List[ReviewerResult] = []
689
- all_reviewers.extend(combined.cli_reviewers.values())
690
- all_reviewers.extend(combined.agents.values())
691
-
692
- issues: List[str] = []
693
- for r in all_reviewers:
694
- if not r.data:
695
- continue
696
- for issue in r.data.get("issues", []):
697
- if issue.get("severity") == severity:
698
- text = issue.get("issue", "").strip()
699
- if text:
700
- issues.append(text)
701
- if len(issues) >= max_count:
702
- break
703
- if len(issues) >= max_count:
704
- break
705
-
706
- if not issues:
707
- return "Review found critical issues"
708
- return "; ".join(issues)
709
-
710
-
711
- def _append_review_details(
712
- lines: List[str],
713
- data: Dict[str, Any],
714
- max_issues: int,
715
- max_missing: int,
716
- max_questions: int
717
- ) -> None:
718
- """Append issue details to markdown lines."""
719
- issues = [i for i in data.get("issues", []) if i.get("severity") != "low"]
720
- if issues:
721
- lines.append("\n**Issues:**")
722
- for it in issues[:max_issues]:
723
- sev = it.get("severity", "medium")
724
- cat = it.get("category", "general")
725
- issue = it.get("issue", "")
726
- fix = it.get("suggested_fix", "")
727
- lines.append(f"- **[{sev}] {cat}**: {issue}")
728
- if fix:
729
- lines.append(f" - fix: {fix}")
730
-
731
- missing = data.get("missing_sections", [])
732
- if missing:
733
- lines.append("\n**Missing Sections:**")
734
- for m in missing[:max_missing]:
735
- lines.append(f"- {m}")
736
-
737
- qs = data.get("questions", [])
738
- if qs:
739
- lines.append("\n**Questions:**")
740
- for q in qs[:max_questions]:
741
- lines.append(f"- {q}")
742
-
743
-
744
- def build_combined_json(result: CombinedReviewResult) -> Dict[str, Any]:
745
- """Build combined JSON output structure."""
746
- output: Dict[str, Any] = {
747
- "metadata": {
748
- "timestamp": result.timestamp,
749
- "plan_hash": result.plan_hash,
750
- },
751
- "overall": {
752
- "verdict": result.overall_verdict,
753
- },
754
- }
755
-
756
- # CLI reviewers
757
- if result.cli_reviewers:
758
- output["cliReviewers"] = {}
759
- for name, r in result.cli_reviewers.items():
760
- output["cliReviewers"][name] = {
761
- "verdict": r.verdict,
762
- "summary": r.data.get("summary") if r.data else None,
763
- "summarySource": r.data.get("summary_source") if r.data else None,
764
- "issues": [i for i in r.data.get("issues", []) if i.get("severity") != "low"] if r.data else [],
765
- "ok": r.ok,
766
- "error": r.err if r.err else None,
767
- }
768
-
769
- # Orchestration
770
- if result.orchestration:
771
- output["orchestration"] = {
772
- "complexity": result.orchestration.complexity,
773
- "category": result.orchestration.category,
774
- "selectedAgents": result.orchestration.selected_agents,
775
- "reasoning": result.orchestration.reasoning,
776
- "skipReason": result.orchestration.skip_reason,
777
- "error": result.orchestration.error,
778
- }
779
-
780
- # Agents
781
- if result.agents:
782
- output["agents"] = {}
783
- for name, r in result.agents.items():
784
- output["agents"][name] = {
785
- "verdict": r.verdict,
786
- "summary": r.data.get("summary") if r.data else None,
787
- "summarySource": r.data.get("summary_source") if r.data else None,
788
- "issues": [i for i in r.data.get("issues", []) if i.get("severity") != "low"] if r.data else [],
789
- "missing_sections": r.data.get("missing_sections", []) if r.data else [],
790
- "questions": r.data.get("questions", []) if r.data else [],
791
- "ok": r.ok,
792
- "error": r.err if r.err else None,
793
- }
794
-
795
- return output
796
-
797
-
798
- def generate_review_index(
799
- result: CombinedReviewResult,
800
- iteration: Optional[int] = None,
801
- settings: Optional[Dict[str, Any]] = None,
802
- ) -> str:
803
- """Generate index.md for a review folder.
804
-
805
- Args:
806
- result: Combined review result
807
- iteration: Iteration number (1-based)
808
- settings: Display settings
809
-
810
- Returns:
811
- Markdown content for index.md
812
- """
813
- from datetime import datetime
814
- now = datetime.now()
815
-
816
- lines = [
817
- "---",
818
- "type: review",
819
- f"plan_hash: {result.plan_hash}",
820
- f"overall_verdict: {result.overall_verdict}",
821
- f"created_at: {result.timestamp}",
822
- ]
823
- if iteration:
824
- lines.append(f"iteration: {iteration}")
825
- lines.extend([
826
- "---",
827
- "",
828
- f"# Plan Review - {now.strftime('%Y-%m-%d %H:%M')}",
829
- "",
830
- f"**Overall Verdict:** `{result.overall_verdict.upper()}`",
831
- ])
832
-
833
- if iteration:
834
- lines.append(f"**Iteration:** {iteration}")
835
-
836
- lines.extend([
837
- f"**Plan Hash:** `{result.plan_hash}`",
838
- "",
839
- ])
840
-
841
- # Summary from orchestrator
842
- if result.orchestration:
843
- lines.extend([
844
- "## Analysis",
845
- f"- **Complexity:** `{result.orchestration.complexity}`",
846
- f"- **Category:** `{result.orchestration.category}`",
847
- f"- **Reasoning:** {result.orchestration.reasoning}",
848
- "",
849
- ])
850
-
851
- # Navigation table
852
- lines.extend([
853
- "## Review Files",
854
- "",
855
- "| File | Description |",
856
- "|------|-------------|",
857
- "| [combined.md](./combined.md) | Full review details |",
858
- "| [combined.json](./combined.json) | Structured review data |",
859
- ])
860
-
861
- # CLI reviewers
862
- for name in result.cli_reviewers.keys():
863
- lines.append(f"| [{name}.json](./{name}.json) | {name.title()} reviewer output |")
864
-
865
- # Agent reviewers
866
- for name in result.agents.keys():
867
- safe_name = sanitize_filename(name)
868
- lines.append(f"| [{safe_name}.json](./{safe_name}.json) | {name} agent output |")
869
-
870
- lines.extend([
871
- "",
872
- "## Verdicts Summary",
873
- "",
874
- "| Reviewer | Verdict |",
875
- "|----------|---------|",
876
- ])
877
-
878
- for name, r in result.cli_reviewers.items():
879
- lines.append(f"| {name.title()} | `{r.verdict}` |")
880
- for name, r in result.agents.items():
881
- lines.append(f"| {name} | `{r.verdict}` |")
882
-
883
- lines.append("")
884
-
885
- return '\n'.join(lines)
886
-
887
-
888
- def write_combined_artifacts(
889
- base: Path,
890
- plan: str,
891
- result: CombinedReviewResult,
892
- payload: Dict[str, Any],
893
- settings: Optional[Dict[str, Any]] = None,
894
- context_reviews_dir: Optional[Path] = None,
895
- review_folder: Optional[Path] = None,
896
- iteration: Optional[int] = None,
897
- ) -> Path:
898
- """Write combined review artifacts to context reviews folder.
899
-
900
- Args:
901
- base: Project base directory
902
- plan: Plan content
903
- result: Combined review result
904
- payload: Hook payload
905
- settings: Display settings
906
- context_reviews_dir: Reviews directory from context system (deprecated, use review_folder)
907
- review_folder: Specific folder to write to (takes precedence)
908
- iteration: Iteration number for index generation
909
-
910
- Raises:
911
- ValueError: If neither context_reviews_dir nor review_folder is provided
912
- """
913
- # Support both old and new API
914
- out_dir = review_folder or context_reviews_dir
915
- if not out_dir:
916
- raise ValueError("Either context_reviews_dir or review_folder is required")
917
-
918
- log_debug("utils", f"Using review folder: {out_dir}")
919
-
920
- # Check directory creation explicitly
921
- try:
922
- out_dir.mkdir(parents=True, exist_ok=True)
923
- except PermissionError as e:
924
- log_error("utils", f"Cannot create directory {out_dir}: {e}")
925
- raise
926
-
927
- # JSON write with atomic operation - use combined.json for folder-based
928
- json_filename = "combined.json" if review_folder else "review.json"
929
- json_path = out_dir / json_filename
930
- json_data = build_combined_json(result)
931
- try:
932
- if ENABLE_ROBUST_PLAN_WRITES:
933
- success, error = atomic_write(json_path, json.dumps(json_data, indent=2, ensure_ascii=False))
934
- if not success:
935
- raise IOError(f"Atomic write failed: {error}")
936
- else:
937
- json_path.write_text(json.dumps(json_data, indent=2, ensure_ascii=False), encoding="utf-8")
938
- except Exception as e:
939
- log_error("utils", f"Failed to write {json_path.name}: {e}")
940
- raise
941
-
942
- # Markdown write with atomic operation - use combined.md for folder-based
943
- md_filename = "combined.md" if review_folder else "review.md"
944
- md_path = out_dir / md_filename
945
- md_content = format_combined_markdown(result, settings)
946
- try:
947
- if ENABLE_ROBUST_PLAN_WRITES:
948
- success, error = atomic_write(md_path, md_content)
949
- if not success:
950
- raise IOError(f"Atomic write failed: {error}")
951
- else:
952
- md_path.write_text(md_content, encoding="utf-8")
953
- except Exception as e:
954
- log_error("utils", f"Failed to write {md_path.name}: {e}")
955
- raise
956
-
957
- # Individual reviewer writes (non-critical - continue on failure)
958
- for name, r in result.cli_reviewers.items():
959
- if r.data:
960
- reviewer_path = out_dir / f"{name}.json"
961
- try:
962
- content = json.dumps(r.data, indent=2, ensure_ascii=False)
963
- if ENABLE_ROBUST_PLAN_WRITES:
964
- success, error = atomic_write(reviewer_path, content)
965
- if not success:
966
- log_warn("utils", f"Failed to write {reviewer_path.name}: {error}")
967
- else:
968
- reviewer_path.write_text(content, encoding="utf-8")
969
- except Exception as e:
970
- log_warn("utils", f"Failed to write {reviewer_path.name}: {e}")
971
- # Continue - individual reviewer failures not critical
972
- for name, r in result.agents.items():
973
- if r.data:
974
- reviewer_path = out_dir / f"{sanitize_filename(name)}.json"
975
- try:
976
- content = json.dumps(r.data, indent=2, ensure_ascii=False)
977
- if ENABLE_ROBUST_PLAN_WRITES:
978
- success, error = atomic_write(reviewer_path, content)
979
- if not success:
980
- log_warn("utils", f"Failed to write {reviewer_path.name}: {error}")
981
- else:
982
- reviewer_path.write_text(content, encoding="utf-8")
983
- except Exception as e:
984
- log_warn("utils", f"Failed to write {reviewer_path.name}: {e}")
985
- # Continue - individual reviewer failures not critical
986
-
987
- # Generate index.md for folder-based reviews
988
- if review_folder:
989
- index_content = generate_review_index(result, iteration, settings)
990
- index_path = out_dir / "index.md"
991
- try:
992
- if ENABLE_ROBUST_PLAN_WRITES:
993
- success, error = atomic_write(index_path, index_content)
994
- if not success:
995
- log_warn("utils", f"Failed to write index.md: {error}")
996
- else:
997
- index_path.write_text(index_content, encoding="utf-8")
998
- except Exception as e:
999
- log_warn("utils", f"Failed to write index.md: {e}")
1000
-
1001
- return index_path
1002
-
1003
- return md_path
1004
-
1005
-
1006
- # ---------------------------
1007
- # Settings loading
1008
- # ---------------------------
1009
-
1010
- def load_config(project_dir: Path) -> Dict[str, Any]:
1011
- """Load full CC-Native config from _cc-native/plan-review.config.json."""
1012
- settings_path = project_dir / "_cc-native" / "plan-review.config.json"
1013
- if not settings_path.exists():
1014
- return {}
1015
- try:
1016
- with open(settings_path, "r", encoding="utf-8") as f:
1017
- return json.load(f)
1018
- except Exception as e:
1019
- log_warn("cc-native", f"Failed to load config: {e}")
1020
- return {}
1021
-
1022
-
1023
- def get_display_settings(config: Dict[str, Any], section: str) -> Dict[str, int]:
1024
- """Get display settings, checking section-specific first, then root."""
1025
- section_display = config.get(section, {}).get("display", {})
1026
- root_display = config.get("display", DEFAULT_DISPLAY)
1027
- return {**DEFAULT_DISPLAY, **root_display, **section_display}