aiwcli 0.10.3 → 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 (189) hide show
  1. package/bin/run.js +1 -1
  2. package/dist/commands/clear.js +28 -131
  3. package/dist/commands/init/index.js +3 -3
  4. package/dist/lib/gitignore-manager.d.ts +32 -0
  5. package/dist/lib/gitignore-manager.js +141 -2
  6. package/dist/templates/CLAUDE.md +8 -8
  7. package/dist/templates/_shared/.claude/commands/handoff-resume.md +64 -0
  8. package/dist/templates/_shared/.claude/commands/handoff.md +16 -10
  9. package/dist/templates/_shared/.claude/settings.json +7 -7
  10. package/dist/templates/_shared/hooks-ts/_utils/git-state.ts +2 -0
  11. package/dist/templates/_shared/hooks-ts/archive_plan.ts +159 -0
  12. package/dist/templates/_shared/hooks-ts/context_monitor.ts +147 -0
  13. package/dist/templates/_shared/hooks-ts/file-suggestion.ts +130 -0
  14. package/dist/templates/_shared/hooks-ts/pre_compact.ts +49 -0
  15. package/dist/templates/_shared/hooks-ts/session_end.ts +104 -0
  16. package/dist/templates/_shared/hooks-ts/session_start.ts +144 -0
  17. package/dist/templates/_shared/hooks-ts/task_create_capture.ts +48 -0
  18. package/dist/templates/_shared/hooks-ts/task_update_capture.ts +74 -0
  19. package/dist/templates/_shared/hooks-ts/user_prompt_submit.ts +83 -0
  20. package/dist/templates/_shared/lib-ts/CLAUDE.md +318 -0
  21. package/dist/templates/_shared/lib-ts/base/atomic-write.ts +12 -12
  22. package/dist/templates/_shared/lib-ts/base/constants.ts +22 -15
  23. package/dist/templates/_shared/lib-ts/base/hook-utils.ts +129 -50
  24. package/dist/templates/_shared/lib-ts/base/inference.ts +28 -21
  25. package/dist/templates/_shared/lib-ts/base/logger.ts +31 -15
  26. package/dist/templates/_shared/lib-ts/base/state-io.ts +9 -7
  27. package/dist/templates/_shared/lib-ts/base/stop-words.ts +131 -131
  28. package/dist/templates/_shared/lib-ts/base/subprocess-utils.ts +139 -0
  29. package/dist/templates/_shared/lib-ts/base/utils.ts +69 -69
  30. package/dist/templates/_shared/lib-ts/context/context-formatter.ts +30 -24
  31. package/dist/templates/_shared/lib-ts/context/context-selector.ts +50 -32
  32. package/dist/templates/_shared/lib-ts/context/context-store.ts +76 -48
  33. package/dist/templates/_shared/lib-ts/context/plan-manager.ts +61 -37
  34. package/dist/templates/_shared/lib-ts/context/task-tracker.ts +10 -6
  35. package/dist/templates/_shared/lib-ts/handoff/document-generator.ts +11 -10
  36. package/dist/templates/_shared/lib-ts/handoff/handoff-reader.ts +159 -0
  37. package/dist/templates/_shared/lib-ts/templates/formatters.ts +6 -4
  38. package/dist/templates/_shared/lib-ts/types.ts +68 -55
  39. package/dist/templates/_shared/scripts/resolve_context.ts +24 -0
  40. package/dist/templates/_shared/scripts/resume_handoff.ts +321 -0
  41. package/dist/templates/_shared/scripts/save_handoff.ts +21 -21
  42. package/dist/templates/_shared/scripts/status_line.ts +733 -0
  43. package/dist/templates/cc-native/.claude/settings.json +175 -185
  44. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +15 -17
  45. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +0 -2
  46. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +109 -135
  47. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.ts +119 -0
  48. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.ts +921 -0
  49. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.ts +61 -0
  50. package/dist/templates/cc-native/_cc-native/lib-ts/aggregate-agents.ts +157 -0
  51. package/dist/templates/cc-native/_cc-native/lib-ts/artifacts.ts +709 -0
  52. package/dist/templates/cc-native/_cc-native/lib-ts/cc-native-state.ts +199 -0
  53. package/dist/templates/cc-native/_cc-native/lib-ts/cli-output-parser.ts +124 -0
  54. package/dist/templates/cc-native/_cc-native/lib-ts/config.ts +57 -0
  55. package/dist/templates/cc-native/_cc-native/lib-ts/constants.ts +83 -0
  56. package/dist/templates/cc-native/_cc-native/lib-ts/debug.ts +80 -0
  57. package/dist/templates/cc-native/_cc-native/lib-ts/index.ts +119 -0
  58. package/dist/templates/cc-native/_cc-native/lib-ts/json-parser.ts +162 -0
  59. package/dist/templates/cc-native/_cc-native/lib-ts/nul +3 -0
  60. package/dist/templates/cc-native/_cc-native/lib-ts/orchestrator.ts +249 -0
  61. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/agent.ts +155 -0
  62. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/codex.ts +130 -0
  63. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/gemini.ts +106 -0
  64. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/index.ts +10 -0
  65. package/dist/templates/cc-native/_cc-native/lib-ts/reviewers/types.ts +23 -0
  66. package/dist/templates/cc-native/_cc-native/lib-ts/state.ts +243 -0
  67. package/dist/templates/cc-native/_cc-native/lib-ts/tsconfig.json +18 -0
  68. package/dist/templates/cc-native/_cc-native/lib-ts/types.ts +310 -0
  69. package/dist/templates/cc-native/_cc-native/lib-ts/verdict.ts +72 -0
  70. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -9
  71. package/oclif.manifest.json +1 -1
  72. package/package.json +1 -1
  73. package/dist/templates/_shared/hooks/__init__.py +0 -16
  74. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  75. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  76. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  77. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  78. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  79. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  80. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  81. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  82. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  83. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  84. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  85. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  86. package/dist/templates/_shared/hooks/archive_plan.py +0 -177
  87. package/dist/templates/_shared/hooks/context_monitor.py +0 -270
  88. package/dist/templates/_shared/hooks/file-suggestion.py +0 -215
  89. package/dist/templates/_shared/hooks/pre_compact.py +0 -104
  90. package/dist/templates/_shared/hooks/session_end.py +0 -173
  91. package/dist/templates/_shared/hooks/session_start.py +0 -206
  92. package/dist/templates/_shared/hooks/task_create_capture.py +0 -108
  93. package/dist/templates/_shared/hooks/task_update_capture.py +0 -145
  94. package/dist/templates/_shared/hooks/user_prompt_submit.py +0 -139
  95. package/dist/templates/_shared/lib/__init__.py +0 -1
  96. package/dist/templates/_shared/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/dist/templates/_shared/lib/base/__init__.py +0 -65
  98. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  99. package/dist/templates/_shared/lib/base/__pycache__/atomic_write.cpython-313.pyc +0 -0
  100. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  101. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  102. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  103. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  104. package/dist/templates/_shared/lib/base/__pycache__/stop_words.cpython-313.pyc +0 -0
  105. package/dist/templates/_shared/lib/base/__pycache__/subprocess_utils.cpython-313.pyc +0 -0
  106. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  107. package/dist/templates/_shared/lib/base/atomic_write.py +0 -180
  108. package/dist/templates/_shared/lib/base/constants.py +0 -358
  109. package/dist/templates/_shared/lib/base/hook_utils.py +0 -339
  110. package/dist/templates/_shared/lib/base/inference.py +0 -307
  111. package/dist/templates/_shared/lib/base/logger.py +0 -305
  112. package/dist/templates/_shared/lib/base/stop_words.py +0 -221
  113. package/dist/templates/_shared/lib/base/subprocess_utils.py +0 -46
  114. package/dist/templates/_shared/lib/base/utils.py +0 -263
  115. package/dist/templates/_shared/lib/context/__init__.py +0 -102
  116. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  117. package/dist/templates/_shared/lib/context/__pycache__/cache.cpython-313.pyc +0 -0
  118. package/dist/templates/_shared/lib/context/__pycache__/context_extractor.cpython-313.pyc +0 -0
  119. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  120. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  121. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  122. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  123. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  124. package/dist/templates/_shared/lib/context/__pycache__/event_log.cpython-313.pyc +0 -0
  125. package/dist/templates/_shared/lib/context/__pycache__/plan_archive.cpython-313.pyc +0 -0
  126. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  127. package/dist/templates/_shared/lib/context/__pycache__/task_sync.cpython-313.pyc +0 -0
  128. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  129. package/dist/templates/_shared/lib/context/context_formatter.py +0 -317
  130. package/dist/templates/_shared/lib/context/context_selector.py +0 -508
  131. package/dist/templates/_shared/lib/context/context_store.py +0 -653
  132. package/dist/templates/_shared/lib/context/plan_manager.py +0 -303
  133. package/dist/templates/_shared/lib/context/task_tracker.py +0 -188
  134. package/dist/templates/_shared/lib/handoff/__init__.py +0 -22
  135. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  136. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  137. package/dist/templates/_shared/lib/handoff/document_generator.py +0 -278
  138. package/dist/templates/_shared/lib/templates/README.md +0 -206
  139. package/dist/templates/_shared/lib/templates/__init__.py +0 -36
  140. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  141. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  142. package/dist/templates/_shared/lib/templates/__pycache__/persona_questions.cpython-313.pyc +0 -0
  143. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  144. package/dist/templates/_shared/lib/templates/formatters.py +0 -146
  145. package/dist/templates/_shared/lib/templates/plan_context.py +0 -73
  146. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  147. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  148. package/dist/templates/_shared/scripts/save_handoff.py +0 -357
  149. package/dist/templates/_shared/scripts/status_line.py +0 -716
  150. package/dist/templates/cc-native/.claude/commands/cc-native/fresh-perspective.md +0 -8
  151. package/dist/templates/cc-native/.windsurf/workflows/cc-native/fresh-perspective.md +0 -8
  152. package/dist/templates/cc-native/MIGRATION.md +0 -86
  153. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  154. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  155. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  156. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  157. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  158. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  159. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +0 -130
  160. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +0 -954
  161. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +0 -81
  162. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +0 -340
  163. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +0 -265
  164. package/dist/templates/cc-native/_cc-native/lib/__init__.py +0 -53
  165. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/dist/templates/cc-native/_cc-native/lib/__pycache__/atomic_write.cpython-313.pyc +0 -0
  167. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  168. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  169. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  170. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  171. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  172. package/dist/templates/cc-native/_cc-native/lib/constants.py +0 -45
  173. package/dist/templates/cc-native/_cc-native/lib/debug.py +0 -139
  174. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +0 -362
  175. package/dist/templates/cc-native/_cc-native/lib/reviewers/__init__.py +0 -28
  176. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  177. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  178. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  179. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  180. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  181. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +0 -215
  182. package/dist/templates/cc-native/_cc-native/lib/reviewers/base.py +0 -88
  183. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +0 -124
  184. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +0 -108
  185. package/dist/templates/cc-native/_cc-native/lib/state.py +0 -268
  186. package/dist/templates/cc-native/_cc-native/lib/utils.py +0 -1071
  187. package/dist/templates/cc-native/_cc-native/scripts/__pycache__/aggregate_agents.cpython-313.pyc +0 -0
  188. package/dist/templates/cc-native/_cc-native/scripts/aggregate_agents.py +0 -168
  189. package/dist/templates/cc-native/_cc-native/workflows/fresh-perspective.md +0 -134
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env python3
2
- """UserPromptSubmit hook - injects Phase A clarification prompt in plan mode.
3
-
4
- On the first prompt in plan mode (before any code exploration), injects
5
- a system-reminder telling Claude to ask clarification questions via
6
- AskUserQuestion before exploring the codebase.
7
-
8
- Skips if questions were already asked this session.
9
- """
10
- import json
11
- import sys
12
- from pathlib import Path
13
-
14
- _hook_dir = Path(__file__).resolve().parent
15
- _cc_native_lib_dir = _hook_dir.parent / "lib"
16
- _shared_lib_dir = _hook_dir.parent.parent / "_shared" / "lib"
17
- sys.path.insert(0, str(_cc_native_lib_dir))
18
- sys.path.insert(0, str(_shared_lib_dir))
19
-
20
- from utils import was_questions_asked
21
- from base.hook_utils import load_hook_input
22
- from base.logger import log_debug, log_info, log_warn, log_error
23
-
24
-
25
- PHASE_A_PROMPT = """
26
- ## Plan Mode: Clarify Before Exploring
27
-
28
- Use AskUserQuestion now — one call, 3-4 questions — before reading any code.
29
-
30
- ### Why This Matters
31
- Once you explore the codebase, you anchor on what you find. Questions asked after exploration confirm your assumptions instead of challenging them. Ask now, while your interpretation is still flexible.
32
-
33
- ### What to Ask About
34
- Only ask about things you cannot discover from code — the user's intent, constraints, history, and priorities:
35
-
36
- - **Ambiguity:** If you can read this request two different ways, ask which interpretation is correct. Provide your top 2-3 readings as options.
37
- - **Invisible context:** What does the user assume "everyone knows" about this system that isn't documented? What's obvious to them but hidden to you?
38
- - **Success criteria:** What does "done well" look like beyond the literal request? What would make them rate this a 10?
39
- - **Constraints and history:** Has this been attempted before? Are there parts of the system that are off-limits or sensitive?
40
-
41
- ### How to Select Questions
42
- 1. Generate 5+ candidate questions across the lenses above
43
- 2. For each, evaluate: "If they answered A vs B, would I explore different files or take a different approach?" If no — discard it.
44
- 3. Keep the 3-4 where different answers lead to meaningfully different exploration strategies
45
- 4. Frame each with 2-3 concrete options so the user can react rather than generate from scratch
46
- """.strip()
47
-
48
-
49
- def main() -> int:
50
- try:
51
- payload = load_hook_input()
52
- if not payload:
53
- return 0
54
-
55
- permission_mode = payload.get("permission_mode", "")
56
- if permission_mode != "plan":
57
- return 0
58
-
59
- session_id = str(payload.get("session_id", ""))
60
- if not session_id:
61
- log_debug("plan_questions_early", "No session_id, skipping")
62
- return 0
63
-
64
- if was_questions_asked(session_id):
65
- log_debug("plan_questions_early", "Questions already asked, skipping")
66
- return 0
67
-
68
- log_info("plan_questions_early", "Plan mode detected, injecting Phase A prompt")
69
- print(f"<system-reminder>{PHASE_A_PROMPT}</system-reminder>")
70
-
71
- except Exception as e:
72
- from base.hook_utils import log_hook_error
73
- log_hook_error("plan_questions_early", e, "UserPromptSubmit")
74
- log_error("plan_questions_early", str(e))
75
-
76
- return 0
77
-
78
-
79
- if __name__ == "__main__":
80
- from base.hook_utils import run_hook
81
- run_hook(main, "plan_questions_early")
@@ -1,340 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- PostToolUse hook - suggests /fresh-perspective when user appears stuck.
4
-
5
- Detection patterns:
6
- 1. Same error appearing 3+ times
7
- 2. Repeated edits to same file without resolution
8
- 3. Test failures after multiple fix attempts
9
-
10
- Behavior: Suggests (doesn't force) running /fresh-perspective.
11
- Non-blocking - always returns success.
12
-
13
- Configuration (in _cc-native/plan-review.config.json):
14
- "stuckDetection": {
15
- "enabled": true, // Set to false to disable entirely
16
- "errorThreshold": 3, // Errors before suggesting
17
- "fileEditThreshold": 4, // Edits to same file before suggesting
18
- "testFailureThreshold": 3, // Test failures before suggesting
19
- "cooldown": 10, // Tool calls between suggestions
20
- "maxSuggestions": 3 // Max suggestions per session
21
- }
22
- """
23
-
24
- import json
25
- import os
26
- import re
27
- import sys
28
- import tempfile
29
- from pathlib import Path
30
- from typing import Any, Dict
31
-
32
- # Add lib directories to path for imports
33
- _hook_dir = Path(__file__).resolve().parent
34
- _lib_dir = _hook_dir.parent / "lib"
35
- _shared_lib = _hook_dir.parent.parent / "_shared" / "lib"
36
- sys.path.insert(0, str(_lib_dir))
37
- sys.path.insert(0, str(_shared_lib))
38
-
39
- from base.hook_utils import emit_context
40
- from base.logger import log_debug, log_info, log_warn, log_error
41
- from utils import sanitize_filename
42
-
43
-
44
- # ---------------------------
45
- # Configuration (defaults, overridden by config.json)
46
- # ---------------------------
47
-
48
- DEFAULT_CONFIG = {
49
- "enabled": True,
50
- "errorThreshold": 3,
51
- "fileEditThreshold": 4,
52
- "testFailureThreshold": 3,
53
- "cooldown": 10,
54
- "maxSuggestions": 3,
55
- }
56
-
57
-
58
- def _int_or_default(value: Any, default: int) -> int:
59
- """Coerce value to int, return default if not possible.
60
-
61
- Handles string numbers, floats, and invalid types gracefully.
62
- """
63
- if isinstance(value, int):
64
- return value
65
- if isinstance(value, float):
66
- return int(value)
67
- if isinstance(value, str):
68
- try:
69
- return int(value)
70
- except ValueError:
71
- return default
72
- return default
73
-
74
-
75
- def load_config(project_dir: Path) -> Dict[str, Any]:
76
- """Load stuckDetection config from _cc-native/plan-review.config.json."""
77
- config_path = project_dir / "_cc-native" / "plan-review.config.json"
78
- if not config_path.exists():
79
- return DEFAULT_CONFIG.copy()
80
- try:
81
- full_config = json.loads(config_path.read_text(encoding="utf-8"))
82
- section = full_config.get("stuckDetection", {})
83
- return {**DEFAULT_CONFIG, **section}
84
- except Exception as e:
85
- log_warn("suggest-fresh-perspective", f"Failed to load config: {e}")
86
- return DEFAULT_CONFIG.copy()
87
-
88
-
89
- def get_project_dir(payload: Dict[str, Any]) -> Path:
90
- """Get project directory from payload or environment."""
91
- p = os.environ.get("CLAUDE_PROJECT_DIR") or payload.get("cwd") or os.getcwd()
92
- return Path(p)
93
-
94
-
95
- # ---------------------------
96
- # Compiled patterns (performance optimization)
97
- # ---------------------------
98
-
99
- # Single combined pattern for error detection (case-insensitive)
100
- _ERROR_PATTERN = re.compile(
101
- r'(error:|failed|exception)',
102
- re.IGNORECASE
103
- )
104
-
105
- # Combined pattern for test failures
106
- _TEST_FAILURE_PATTERN = re.compile(
107
- r'(\d+\s+failed|FAIL\s|✗|AssertionError|test.*failed|npm\s+ERR!.*test)',
108
- re.IGNORECASE
109
- )
110
-
111
- # Pattern for normalizing error messages (line numbers)
112
- _LINE_NUMBER_PATTERN = re.compile(r':\d+')
113
- _MULTI_DIGIT_PATTERN = re.compile(r'\d{2,}')
114
- _PATH_PATTERN = re.compile(r'[/\\][^\s/\\]+[/\\]')
115
-
116
-
117
- # ---------------------------
118
- # State management (session-scoped)
119
- # ---------------------------
120
-
121
- def get_state_path(session_id: str) -> Path:
122
- """Get path to stuck-detection state file for this session."""
123
- safe_id = sanitize_filename(str(session_id), max_len=32)
124
- return Path(tempfile.gettempdir()) / f"cc-native-stuck-state-{safe_id}.json"
125
-
126
-
127
- def load_state(session_id: str) -> Dict[str, Any]:
128
- """Load stuck detection state for this session."""
129
- state_path = get_state_path(session_id)
130
- default_state = {
131
- "error_hashes": {}, # hash -> count
132
- "file_edits": {}, # file_path -> count
133
- "test_failures": 0,
134
- "tool_calls_since_suggestion": 0,
135
- "suggestion_count": 0,
136
- }
137
- if not state_path.exists():
138
- return default_state
139
- try:
140
- return json.loads(state_path.read_text(encoding="utf-8"))
141
- except Exception:
142
- return default_state
143
-
144
-
145
- def save_state(session_id: str, state: Dict[str, Any]) -> None:
146
- """Save stuck detection state for this session."""
147
- state_path = get_state_path(session_id)
148
- try:
149
- state_path.write_text(json.dumps(state), encoding="utf-8")
150
- except Exception as e:
151
- log_warn("suggest-fresh-perspective", f"Failed to save state: {e}")
152
-
153
-
154
- # ---------------------------
155
- # Detection logic
156
- # ---------------------------
157
-
158
- def hash_error(error_text: str) -> str:
159
- """Create a simple hash of an error message for deduplication.
160
-
161
- Normalizes by removing line numbers and multi-digit numbers,
162
- but preserves enough context to distinguish different errors.
163
- """
164
- # Normalize: remove line numbers, preserve error type
165
- normalized = _LINE_NUMBER_PATTERN.sub(':N', error_text)
166
- normalized = _MULTI_DIGIT_PATTERN.sub('N', normalized)
167
- # Simplify paths but keep some structure
168
- normalized = _PATH_PATTERN.sub('.../', normalized)
169
- # Take first 100 chars after normalization
170
- return normalized[:100]
171
-
172
-
173
- def detect_repeated_error(state: Dict[str, Any], tool_result: str, threshold: int) -> bool:
174
- """Check if we're seeing the same error repeatedly.
175
-
176
- Returns True if threshold reached, always updates state.
177
- """
178
- if not tool_result:
179
- return False
180
-
181
- if _ERROR_PATTERN.search(tool_result):
182
- error_hash = hash_error(tool_result)
183
- state["error_hashes"][error_hash] = state["error_hashes"].get(error_hash, 0) + 1
184
- return state["error_hashes"][error_hash] >= threshold
185
-
186
- return False
187
-
188
-
189
- def detect_repeated_file_edits(state: Dict[str, Any], tool_name: str, tool_input: Dict[str, Any], threshold: int) -> bool:
190
- """Check if we're editing the same file repeatedly.
191
-
192
- Returns True if threshold reached, always updates state.
193
- """
194
- if tool_name != "Edit":
195
- return False
196
-
197
- # Validate tool_input is a dict
198
- if not isinstance(tool_input, dict):
199
- return False
200
-
201
- file_path = tool_input.get("file_path", "")
202
- if not file_path:
203
- return False
204
-
205
- state["file_edits"][file_path] = state["file_edits"].get(file_path, 0) + 1
206
- return state["file_edits"][file_path] >= threshold
207
-
208
-
209
- def detect_test_failures(state: Dict[str, Any], tool_name: str, tool_result: str, threshold: int) -> bool:
210
- """Check for repeated test failures.
211
-
212
- Returns True if threshold reached, always updates state.
213
- """
214
- if tool_name != "Bash":
215
- return False
216
-
217
- if _TEST_FAILURE_PATTERN.search(tool_result):
218
- state["test_failures"] = state.get("test_failures", 0) + 1
219
- return state["test_failures"] >= threshold
220
-
221
- return False
222
-
223
-
224
- # ---------------------------
225
- # Main hook logic
226
- # ---------------------------
227
-
228
- def should_suggest(state: Dict[str, Any], cooldown: int) -> bool:
229
- """Check if we're past the cooldown period."""
230
- return state.get("tool_calls_since_suggestion", 0) >= cooldown
231
-
232
-
233
- def create_suggestion() -> None:
234
- """Emit the suggestion via hook utility."""
235
- emit_context(
236
- "\n---\n"
237
- "**Stuck?** You've been working on similar issues for a while. "
238
- "Consider running `/fresh-perspective` to get an unbiased view of the problem "
239
- "without code context anchoring your thinking.\n"
240
- "---\n"
241
- )
242
-
243
-
244
- def main() -> int:
245
- # === FAST PATH: Cheap checks first, no I/O ===
246
-
247
- try:
248
- payload = json.load(sys.stdin)
249
- except json.JSONDecodeError:
250
- return 0 # Fail-safe
251
-
252
- # 1. Check hook_event_name (cheap dict lookup)
253
- if payload.get("hook_event_name") != "PostToolUse":
254
- return 0
255
-
256
- # 2. Check session_id exists (cheap dict lookup)
257
- session_id = payload.get("session_id")
258
- if not session_id:
259
- return 0
260
-
261
- # 3. Check tool_name is relevant (cheap dict lookup)
262
- # We only care about Edit and Bash - skip everything else
263
- tool_name = payload.get("tool_name", "")
264
- if tool_name not in ("Edit", "Bash"):
265
- return 0
266
-
267
- # === SLOW PATH: Only reached for Edit/Bash tools ===
268
-
269
- # Load configuration (file I/O)
270
- project_dir = get_project_dir(payload)
271
- config = load_config(project_dir)
272
-
273
- # Check if feature is disabled
274
- if not config.get("enabled", True):
275
- return 0
276
-
277
- tool_input = payload.get("tool_input", {})
278
- tool_result = payload.get("tool_result", {})
279
-
280
- # Validate tool_input type
281
- if not isinstance(tool_input, dict):
282
- tool_input = {}
283
-
284
- # Extract result text
285
- result_text = ""
286
- if isinstance(tool_result, dict):
287
- result_text = str(tool_result.get("output", "") or tool_result.get("content", ""))
288
- elif isinstance(tool_result, str):
289
- result_text = tool_result
290
-
291
- # Load state (file I/O)
292
- state = load_state(session_id)
293
-
294
- # Increment tool call counter
295
- state["tool_calls_since_suggestion"] = state.get("tool_calls_since_suggestion", 0) + 1
296
-
297
- # Get thresholds from config (with type coercion for safety)
298
- error_threshold = _int_or_default(config.get("errorThreshold"), 3)
299
- file_edit_threshold = _int_or_default(config.get("fileEditThreshold"), 4)
300
- test_failure_threshold = _int_or_default(config.get("testFailureThreshold"), 3)
301
- cooldown = _int_or_default(config.get("cooldown"), 10)
302
- max_suggestions = _int_or_default(config.get("maxSuggestions"), 3)
303
-
304
- # Run ALL detections (don't short-circuit - each updates state)
305
- error_detected = detect_repeated_error(state, result_text, error_threshold)
306
- file_edit_detected = detect_repeated_file_edits(state, tool_name, tool_input, file_edit_threshold)
307
- test_failure_detected = detect_test_failures(state, tool_name, result_text, test_failure_threshold)
308
-
309
- # Save state AFTER all detections have run
310
- save_state(session_id, state)
311
-
312
- # Check if any detection triggered
313
- is_stuck = error_detected or file_edit_detected or test_failure_detected
314
-
315
- if is_stuck:
316
- if error_detected:
317
- log_info("suggest-fresh-perspective", "Detected repeated error pattern")
318
- if file_edit_detected:
319
- log_info("suggest-fresh-perspective", "Detected repeated file edits")
320
- if test_failure_detected:
321
- log_info("suggest-fresh-perspective", "Detected repeated test failures")
322
-
323
- # Only suggest if stuck AND past cooldown
324
- if is_stuck and should_suggest(state, cooldown):
325
- # Reset cooldown
326
- state["tool_calls_since_suggestion"] = 0
327
- state["suggestion_count"] = state.get("suggestion_count", 0) + 1
328
- save_state(session_id, state)
329
-
330
- # Only suggest up to maxSuggestions times per session
331
- if state["suggestion_count"] <= max_suggestions:
332
- log_info("suggest-fresh-perspective", f"Suggesting fresh perspective (suggestion #{state['suggestion_count']})")
333
- create_suggestion()
334
-
335
- return 0
336
-
337
-
338
- if __name__ == "__main__":
339
- from base.hook_utils import run_hook
340
- run_hook(main, "suggest_fresh_perspective")
@@ -1,265 +0,0 @@
1
- # CC-Native Library Development Guide
2
-
3
- > **Keep this document updated.** When you solve an issue related to library code, add the solution to the relevant section and log it in the Changelog. This document should grow with discovered patterns and fixes—don't wait to be asked.
4
-
5
- ---
6
-
7
- ## Module Overview
8
-
9
- | Module | Purpose |
10
- |--------|---------|
11
- | `utils.py` | Core utilities: eprint, sanitize, JSON parsing, artifact writing |
12
- | `state.py` | Plan state file management and iteration tracking |
13
- | `orchestrator.py` | Plan complexity analysis and agent selection |
14
- | `reviewers/` | Plan review implementations (package — see below) |
15
- | `constants.py` | Shared constants and feature flags (e.g., `ENABLE_ROBUST_PLAN_WRITES`) |
16
- | `debug.py` | Permanent debug logging to context folder (`CCNATIVE_DEBUG_DISABLE=1` to disable) |
17
- | `__init__.py` | Package exports |
18
-
19
- ### reviewers/ Package
20
-
21
- | File | Purpose |
22
- |------|---------|
23
- | `__init__.py` | Re-exports: `ReviewerResult`, `run_codex_review`, `run_gemini_review`, `run_agent_review` |
24
- | `base.py` | `ReviewerResult`, `REVIEW_SCHEMA`, `AgentConfig`, `OrchestratorConfig` |
25
- | `agent.py` | Claude Code agent-based reviewer (uses `--system-prompt`) |
26
- | `codex.py` | Codex CLI reviewer |
27
- | `gemini.py` | Google Gemini API reviewer |
28
-
29
- ---
30
-
31
- ## Dependency Graph
32
-
33
- ```
34
- Hooks (cc-native-plan-review.py, etc.)
35
-
36
- ├── lib/utils.py (core utilities)
37
- │ └── lib/constants.py
38
- │ └── _shared/lib/base/atomic_write.py
39
-
40
- ├── lib/state.py (state management)
41
- │ └── lib/utils.py (eprint)
42
-
43
- ├── lib/orchestrator.py (agent selection)
44
- │ └── lib/utils.py (ReviewerResult, etc.)
45
-
46
- ├── lib/reviewers/ (plan review package)
47
- │ ├── base.py (ReviewerResult, AgentConfig, schemas)
48
- │ ├── agent.py → base.py
49
- │ ├── codex.py → base.py
50
- │ └── gemini.py → base.py
51
-
52
- ├── lib/debug.py (context-folder debug logging)
53
-
54
- └── _shared/lib/ (shared across all methods)
55
- ├── lib/base/subprocess_utils.py
56
- ├── lib/base/constants.py
57
- └── lib/context/context_manager.py
58
- ```
59
-
60
- **Import direction:** Hooks → cc-native lib → shared lib. Never the reverse.
61
-
62
- ---
63
-
64
- ## Key Data Classes
65
-
66
- ### ReviewerResult
67
-
68
- ```python
69
- @dataclass
70
- class ReviewerResult:
71
- name: str # Reviewer name (e.g., "codex", "architect-reviewer")
72
- ok: bool # True if review completed successfully
73
- verdict: str # "pass" | "warn" | "fail" | "error" | "skip"
74
- data: Dict[str, Any] # Structured review data (summary, issues, etc.)
75
- raw: str # Raw response text
76
- err: str # Error message if any
77
- ```
78
-
79
- ### OrchestratorResult
80
-
81
- ```python
82
- @dataclass
83
- class OrchestratorResult:
84
- complexity: str # "simple" | "medium" | "high"
85
- category: str # "code" | "infrastructure" | "documentation" | etc.
86
- selected_agents: List[str] # Agent names to run
87
- reasoning: str # Why these agents were selected
88
- skip_reason: Optional[str] # Why review was skipped (if applicable)
89
- error: Optional[str] # Error message if orchestrator failed
90
- ```
91
-
92
- ### CombinedReviewResult
93
-
94
- ```python
95
- @dataclass
96
- class CombinedReviewResult:
97
- plan_hash: str # SHA256 hash (first 16 chars)
98
- overall_verdict: str # Worst verdict across all reviewers
99
- cli_reviewers: Dict[str, ReviewerResult] # Codex, Gemini results
100
- orchestration: Optional[OrchestratorResult]
101
- agents: Dict[str, ReviewerResult] # Agent review results
102
- timestamp: str # ISO format
103
- ```
104
-
105
- ---
106
-
107
- ## Windows Path Handling
108
-
109
- Windows uses backslashes in paths. Always normalize when comparing:
110
-
111
- ```python
112
- # CORRECT - works on Windows and Unix
113
- if ".claude/plans/" in file_path.replace("\\", "/"):
114
- # Found a plan file
115
-
116
- # Also correct - use Path for comparisons
117
- from pathlib import Path
118
- if Path(".claude/plans") in Path(file_path).parents:
119
- # Found a plan file
120
- ```
121
-
122
- ```python
123
- # WRONG - fails on Windows
124
- if ".claude/plans/" in file_path: # Windows path: ".claude\\plans\\"
125
- # Never matches on Windows!
126
- ```
127
-
128
- This is a recurring issue. Any path string comparison must handle both separators.
129
-
130
- ---
131
-
132
- ## Atomic Writes
133
-
134
- For critical files (state, reviews), use atomic writes to prevent corruption on crash:
135
-
136
- ```python
137
- # Import from shared lib (canonical location)
138
- from _shared.lib.base.atomic_write import atomic_write
139
-
140
- # CORRECT - atomic write
141
- success, error = atomic_write(path, content)
142
- if not success:
143
- eprint(f"[module] Write failed: {error}")
144
- ```
145
-
146
- ```python
147
- # RISKY - can leave partial file on crash
148
- path.write_text(content, encoding="utf-8")
149
- ```
150
-
151
- Atomic writes use a temp file + rename pattern. The `constants.ENABLE_ROBUST_PLAN_WRITES` feature flag (env: `CC_NATIVE_ROBUST_WRITES`, default: `true`) controls whether atomic writes are used for plan state files.
152
-
153
- ---
154
-
155
- ## Adding New Reviewers
156
-
157
- 1. **Create reviewer file** in `reviewers/` package (e.g., `reviewers/myreviewer.py`):
158
- ```python
159
- from .base import ReviewerResult, REVIEW_SCHEMA
160
-
161
- def run_myreviewer_review(
162
- plan: str,
163
- schema: Dict[str, Any],
164
- settings: Dict[str, Any],
165
- ) -> ReviewerResult:
166
- # Implementation
167
- return ReviewerResult(
168
- name="myreviewer",
169
- ok=True,
170
- verdict="pass",
171
- data=parsed_data,
172
- raw=raw_response,
173
- err="",
174
- )
175
- ```
176
-
177
- 2. **Export in `reviewers/__init__.py`**:
178
- ```python
179
- from .myreviewer import run_myreviewer_review
180
- ```
181
-
182
- 3. **Add config** in `plan-review.config.json`:
183
- ```json
184
- {
185
- "planReview": {
186
- "reviewers": {
187
- "myreviewer": {"enabled": true, "timeout": 120}
188
- }
189
- }
190
- }
191
- ```
192
-
193
- 4. **Wire in hook** (`cc-native-plan-review.py`):
194
- ```python
195
- from reviewers import run_myreviewer_review
196
-
197
- if myreviewer_enabled:
198
- phase1_tasks.append(("myreviewer", lambda: run_myreviewer_review(...)))
199
- ```
200
-
201
- ---
202
-
203
- ## JSON Parsing
204
-
205
- Use `parse_json_maybe` for LLM responses - it handles markdown code blocks and extraction:
206
-
207
- ```python
208
- from utils import parse_json_maybe, coerce_to_review
209
-
210
- # Parse with field validation
211
- obj = parse_json_maybe(raw_response, require_fields=["verdict", "summary"])
212
-
213
- # Normalize to expected structure
214
- ok, verdict, data = coerce_to_review(obj)
215
- ```
216
-
217
- The parser tries:
218
- 1. Strict JSON parse
219
- 2. Extract `{...}` block from text (handles ```json blocks)
220
-
221
- ---
222
-
223
- ## Encoding
224
-
225
- Always specify encoding on file operations:
226
-
227
- ```python
228
- # CORRECT
229
- content = path.read_text(encoding="utf-8")
230
- path.write_text(content, encoding="utf-8")
231
-
232
- with open(path, "r", encoding="utf-8") as f:
233
- data = json.load(f)
234
- ```
235
-
236
- ```python
237
- # WRONG - uses system default (can fail on Windows)
238
- content = path.read_text() # May use cp1252 on Windows
239
- ```
240
-
241
- ---
242
-
243
- ## DO NOT
244
-
245
- These are reminders based on past issues. Not enforcement rules.
246
-
247
- - **Don't import from `_cc-native/lib/` in `_shared/lib/`** - wrong direction, creates circular deps
248
- - **Don't use `print()` for debugging** - use `log_debug/log_info/log_warn/log_error` from `_shared/lib/base/logger.py` (writes to stderr + `_output/hook-log.jsonl`)
249
- - **Don't modify data class fields** without updating all consumers (hooks, formatters, tests)
250
- - **Don't hardcode paths** - use `Path(__file__)`, env vars, or config
251
- - **Don't forget `encoding="utf-8"`** on file operations - Windows defaults are unsafe
252
- - **Don't assume forward slashes** in file paths - Windows uses backslashes
253
- - **Don't skip atomic writes** for critical state files - use `atomic_write` function
254
-
255
- ---
256
-
257
- ## Changelog
258
-
259
- <!-- Add dated entries as new issues are discovered -->
260
-
261
- | Date | Change |
262
- |------|--------|
263
- | 2026-02-07 | Unified logger: all diagnostic logging uses `_shared/lib/base/logger.py` instead of eprint/print-to-stderr |
264
- | 2026-02-06 | Remove duplicate `atomic_write.py` — consolidated to `_shared/lib/base/atomic_write.py` |
265
- | 2026-02-03 | Initial creation |