aiwcli 0.9.7 → 0.10.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 (119) hide show
  1. package/bin/run.js +5 -2
  2. package/dist/lib/claude-settings-types.d.ts +2 -0
  3. package/dist/templates/CLAUDE.md +49 -18
  4. package/dist/templates/_shared/.claude/settings.json +4 -0
  5. package/dist/templates/_shared/hooks/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/dist/templates/_shared/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  7. package/dist/templates/_shared/hooks/__pycache__/context_enforcer.cpython-313.pyc +0 -0
  8. package/dist/templates/_shared/hooks/__pycache__/context_monitor.cpython-313.pyc +0 -0
  9. package/dist/templates/_shared/hooks/__pycache__/file-suggestion.cpython-313.pyc +0 -0
  10. package/dist/templates/_shared/hooks/__pycache__/pre_compact.cpython-313.pyc +0 -0
  11. package/dist/templates/_shared/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  12. package/dist/templates/_shared/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  13. package/dist/templates/_shared/hooks/__pycache__/task_create_atomicity.cpython-313.pyc +0 -0
  14. package/dist/templates/_shared/hooks/__pycache__/task_create_capture.cpython-313.pyc +0 -0
  15. package/dist/templates/_shared/hooks/__pycache__/task_update_capture.cpython-313.pyc +0 -0
  16. package/dist/templates/_shared/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  17. package/dist/templates/_shared/hooks/archive_plan.py +87 -178
  18. package/dist/templates/_shared/hooks/context_monitor.py +128 -194
  19. package/dist/templates/_shared/hooks/file-suggestion.py +26 -23
  20. package/dist/templates/_shared/hooks/pre_compact.py +104 -0
  21. package/dist/templates/_shared/hooks/session_end.py +154 -0
  22. package/dist/templates/_shared/hooks/session_start.py +145 -59
  23. package/dist/templates/_shared/hooks/task_create_capture.py +26 -49
  24. package/dist/templates/_shared/hooks/task_update_capture.py +42 -100
  25. package/dist/templates/_shared/hooks/user_prompt_submit.py +63 -77
  26. package/dist/templates/_shared/lib/base/__init__.py +16 -0
  27. package/dist/templates/_shared/lib/base/__pycache__/__init__.cpython-313.pyc +0 -0
  28. package/dist/templates/_shared/lib/base/__pycache__/constants.cpython-313.pyc +0 -0
  29. package/dist/templates/_shared/lib/base/__pycache__/hook_utils.cpython-313.pyc +0 -0
  30. package/dist/templates/_shared/lib/base/__pycache__/inference.cpython-313.pyc +0 -0
  31. package/dist/templates/_shared/lib/base/__pycache__/logger.cpython-313.pyc +0 -0
  32. package/dist/templates/_shared/lib/base/__pycache__/utils.cpython-313.pyc +0 -0
  33. package/dist/templates/_shared/lib/base/constants.py +18 -4
  34. package/dist/templates/_shared/lib/base/hook_utils.py +199 -11
  35. package/dist/templates/_shared/lib/base/inference.py +121 -0
  36. package/dist/templates/_shared/lib/base/logger.py +291 -0
  37. package/dist/templates/_shared/lib/base/utils.py +49 -11
  38. package/dist/templates/_shared/lib/context/__init__.py +72 -80
  39. package/dist/templates/_shared/lib/context/__pycache__/__init__.cpython-313.pyc +0 -0
  40. package/dist/templates/_shared/lib/context/__pycache__/context_formatter.cpython-313.pyc +0 -0
  41. package/dist/templates/_shared/lib/context/__pycache__/context_manager.cpython-313.pyc +0 -0
  42. package/dist/templates/_shared/lib/context/__pycache__/context_selector.cpython-313.pyc +0 -0
  43. package/dist/templates/_shared/lib/context/__pycache__/context_store.cpython-313.pyc +0 -0
  44. package/dist/templates/_shared/lib/context/__pycache__/discovery.cpython-313.pyc +0 -0
  45. package/dist/templates/_shared/lib/context/__pycache__/plan_manager.cpython-313.pyc +0 -0
  46. package/dist/templates/_shared/lib/context/__pycache__/task_tracker.cpython-313.pyc +0 -0
  47. package/dist/templates/_shared/lib/context/context_formatter.py +316 -0
  48. package/dist/templates/_shared/lib/context/context_selector.py +491 -0
  49. package/dist/templates/_shared/lib/context/context_store.py +636 -0
  50. package/dist/templates/_shared/lib/context/plan_manager.py +204 -0
  51. package/dist/templates/_shared/lib/context/task_tracker.py +188 -0
  52. package/dist/templates/_shared/lib/handoff/__pycache__/__init__.cpython-313.pyc +0 -0
  53. package/dist/templates/_shared/lib/handoff/__pycache__/document_generator.cpython-313.pyc +0 -0
  54. package/dist/templates/_shared/lib/handoff/document_generator.py +14 -40
  55. package/dist/templates/_shared/lib/templates/README.md +5 -13
  56. package/dist/templates/_shared/lib/templates/__init__.py +2 -6
  57. package/dist/templates/_shared/lib/templates/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/dist/templates/_shared/lib/templates/__pycache__/formatters.cpython-313.pyc +0 -0
  59. package/dist/templates/_shared/lib/templates/__pycache__/plan_context.cpython-313.pyc +0 -0
  60. package/dist/templates/_shared/lib/templates/plan_context.py +25 -79
  61. package/dist/templates/_shared/scripts/__pycache__/save_handoff.cpython-313.pyc +0 -0
  62. package/dist/templates/_shared/scripts/__pycache__/status_line.cpython-313.pyc +0 -0
  63. package/dist/templates/_shared/scripts/save_handoff.py +39 -19
  64. package/dist/templates/_shared/scripts/status_line.py +701 -0
  65. package/dist/templates/_shared/workflows/handoff.md +9 -3
  66. package/dist/templates/cc-native/.claude/settings.json +64 -9
  67. package/dist/templates/cc-native/CC-NATIVE-README.md +25 -28
  68. package/dist/templates/cc-native/MIGRATION.md +1 -1
  69. package/dist/templates/cc-native/TEMPLATE-SCHEMA.md +14 -39
  70. package/dist/templates/cc-native/_cc-native/agents/CLAUDE.md +1 -1
  71. package/dist/templates/cc-native/_cc-native/hooks/CLAUDE.md +57 -22
  72. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/add_plan_context.cpython-313.pyc +0 -0
  73. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-plan-review.cpython-313.pyc +0 -0
  74. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/mark_questions_asked.cpython-313.pyc +0 -0
  75. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_accepted.cpython-313.pyc +0 -0
  76. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/plan_questions_early.cpython-313.pyc +0 -0
  77. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/suggest-fresh-perspective.cpython-313.pyc +0 -0
  78. package/dist/templates/cc-native/_cc-native/hooks/add_plan_context.py +57 -57
  79. package/dist/templates/cc-native/_cc-native/hooks/cc-native-plan-review.py +208 -158
  80. package/dist/templates/cc-native/_cc-native/hooks/plan_accepted.py +127 -0
  81. package/dist/templates/cc-native/_cc-native/hooks/plan_questions_early.py +81 -0
  82. package/dist/templates/cc-native/_cc-native/hooks/suggest-fresh-perspective.py +26 -25
  83. package/dist/templates/cc-native/_cc-native/lib/CLAUDE.md +35 -10
  84. package/dist/templates/cc-native/_cc-native/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/dist/templates/cc-native/_cc-native/lib/__pycache__/constants.cpython-313.pyc +0 -0
  86. package/dist/templates/cc-native/_cc-native/lib/__pycache__/debug.cpython-313.pyc +0 -0
  87. package/dist/templates/cc-native/_cc-native/lib/__pycache__/orchestrator.cpython-313.pyc +0 -0
  88. package/dist/templates/cc-native/_cc-native/lib/__pycache__/state.cpython-313.pyc +0 -0
  89. package/dist/templates/cc-native/_cc-native/lib/__pycache__/utils.cpython-313.pyc +0 -0
  90. package/dist/templates/cc-native/_cc-native/lib/debug.py +37 -22
  91. package/dist/templates/cc-native/_cc-native/lib/orchestrator.py +103 -42
  92. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/__init__.cpython-313.pyc +0 -0
  93. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/agent.cpython-313.pyc +0 -0
  94. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/base.cpython-313.pyc +0 -0
  95. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/codex.cpython-313.pyc +0 -0
  96. package/dist/templates/cc-native/_cc-native/lib/reviewers/__pycache__/gemini.cpython-313.pyc +0 -0
  97. package/dist/templates/cc-native/_cc-native/lib/reviewers/agent.py +26 -21
  98. package/dist/templates/cc-native/_cc-native/lib/reviewers/codex.py +12 -7
  99. package/dist/templates/cc-native/_cc-native/lib/reviewers/gemini.py +12 -7
  100. package/dist/templates/cc-native/_cc-native/lib/state.py +31 -16
  101. package/dist/templates/cc-native/_cc-native/lib/utils.py +210 -43
  102. package/dist/templates/cc-native/_cc-native/plan-review.config.json +1 -2
  103. package/oclif.manifest.json +1 -1
  104. package/package.json +1 -1
  105. package/dist/templates/_shared/hooks/context_enforcer.py +0 -625
  106. package/dist/templates/_shared/hooks/task_create_atomicity.py +0 -205
  107. package/dist/templates/_shared/lib/context/cache.py +0 -444
  108. package/dist/templates/_shared/lib/context/context_extractor.py +0 -115
  109. package/dist/templates/_shared/lib/context/context_manager.py +0 -1054
  110. package/dist/templates/_shared/lib/context/discovery.py +0 -444
  111. package/dist/templates/_shared/lib/context/event_log.py +0 -308
  112. package/dist/templates/_shared/lib/context/plan_archive.py +0 -101
  113. package/dist/templates/_shared/lib/context/task_sync.py +0 -290
  114. package/dist/templates/_shared/lib/templates/persona_questions.py +0 -113
  115. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/archive_plan.cpython-313.pyc +0 -0
  116. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/cc-native-agent-review.cpython-313.pyc +0 -0
  117. package/dist/templates/cc-native/_cc-native/hooks/__pycache__/test_permission_request.cpython-313.pyc +0 -0
  118. package/dist/templates/cc-native/_cc-native/lib/async_archive.py +0 -68
  119. package/dist/templates/cc-native/_cc-native/lib/atomic_write.py +0 -98
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """PostToolUse hook for ExitPlanMode — assigns plan fields to state.json.
3
+
4
+ This hook fires when the user ACCEPTS the plan (ExitPlanMode succeeds).
5
+ PostToolUse fires ONLY on success, so this reliably indicates acceptance.
6
+
7
+ The plan was already archived by archive_plan.py (PermissionRequest).
8
+ This hook finds the archived plan, computes hash + signature, and assigns
9
+ plan_hash, plan_signature, plan_path to state.json — without changing mode.
10
+
11
+ Separation of concerns:
12
+ - archive_plan.py (PermissionRequest) -> archives file only, no state.json changes
13
+ - plan_accepted.py (PostToolUse) -> assigns plan fields (hash/signature/path)
14
+ - session_end.py (SessionEnd) -> transitions active -> has_plan when plan is assigned
15
+ - context_selector.py -> matches plan content, transitions has_plan -> active
16
+
17
+ Usage in .claude/settings.json:
18
+ {
19
+ "hooks": {
20
+ "PostToolUse": [{
21
+ "matcher": "ExitPlanMode",
22
+ "hooks": [{
23
+ "type": "command",
24
+ "command": "python .aiwcli/_cc-native/hooks/plan_accepted.py",
25
+ "timeout": 5000
26
+ }]
27
+ }]
28
+ }
29
+ }
30
+ """
31
+ import hashlib
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ # Add parent directories to path for imports
36
+ SCRIPT_DIR = Path(__file__).resolve().parent
37
+ SHARED_LIB = SCRIPT_DIR.parent.parent / "_shared" / "lib"
38
+ sys.path.insert(0, str(SHARED_LIB.parent))
39
+
40
+ from lib.base.hook_utils import load_hook_input, log_hook_error
41
+ from lib.base.logger import log_info, log_debug, log_warn, log_error
42
+ from lib.base.utils import project_dir
43
+ from lib.context.context_store import get_context_by_session_id, update_mode
44
+ from lib.context.plan_manager import (
45
+ find_latest_plan,
46
+ extract_plan_path_from_result,
47
+ generate_plan_id,
48
+ normalize_plan_content,
49
+ extract_plan_anchors,
50
+ )
51
+
52
+
53
+ def main():
54
+ """Assign plan fields (hash/signature/path) to state.json on plan acceptance."""
55
+ hook_input = load_hook_input()
56
+ if not hook_input:
57
+ log_warn("plan_accepted", "EXIT: no hook_input (stdin empty or invalid JSON)")
58
+ return
59
+
60
+ hook_event = hook_input.get("hook_event_name", "")
61
+ tool_name = hook_input.get("tool_name", "")
62
+ session_id = hook_input.get("session_id", "MISSING")
63
+
64
+ if not (hook_event == "PostToolUse" and tool_name == "ExitPlanMode"):
65
+ log_debug("plan_accepted", f"Skipping: {hook_event}/{tool_name}")
66
+ return
67
+
68
+ project_root = project_dir(hook_input)
69
+ state = get_context_by_session_id(session_id, project_root)
70
+ if not state:
71
+ log_warn("plan_accepted", f"No context for session {session_id}")
72
+ return
73
+
74
+ log_debug("plan_accepted", f"Found context: {state.id}, mode: {state.mode}")
75
+
76
+ # Find the latest archived plan
77
+ plan_path = find_latest_plan(state.id, project_root)
78
+ if not plan_path:
79
+ log_warn("plan_accepted", f"No archived plan found for {state.id}")
80
+ return
81
+
82
+ # Find the original plan file (in ~/.claude/plans/) to inject plan-id there too
83
+ original_plan_path = extract_plan_path_from_result(hook_input.get("tool_result", ""))
84
+ if not original_plan_path:
85
+ claude_plans_dir = Path.home() / ".claude" / "plans"
86
+ if claude_plans_dir.exists():
87
+ plans = sorted(claude_plans_dir.glob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True)
88
+ if plans:
89
+ original_plan_path = str(plans[0])
90
+
91
+ # Generate plan ID and inject into plan files
92
+ plan_id = generate_plan_id()
93
+ id_marker = f"<!-- plan-id: {plan_id} -->\n"
94
+
95
+ for file_path in [plan_path, original_plan_path]:
96
+ if file_path and Path(file_path).exists():
97
+ file_content = Path(file_path).read_text(encoding="utf-8")
98
+ if "<!-- plan-id:" not in file_content:
99
+ Path(file_path).write_text(id_marker + file_content, encoding="utf-8")
100
+
101
+ # Read the modified content (with plan ID) for hashing
102
+ content = Path(plan_path).read_text(encoding="utf-8")
103
+
104
+ # Compute normalized hash (Tier 2)
105
+ normalized = normalize_plan_content(content)
106
+ plan_hash = hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:12]
107
+ plan_signature = content[:200] # Keep for backward compat
108
+
109
+ # Extract structural anchors (Tier 3)
110
+ plan_anchors = extract_plan_anchors(content)
111
+
112
+ # Assign plan to context (no mode change — keep current mode)
113
+ update_mode(
114
+ state.id, state.mode,
115
+ project_root=project_root,
116
+ plan_path=plan_path,
117
+ plan_hash=plan_hash,
118
+ plan_signature=plan_signature,
119
+ plan_id=plan_id,
120
+ plan_anchors=plan_anchors,
121
+ )
122
+ log_info("plan_accepted", f"Assigned plan to {state.id} (id: {plan_id}, hash: {plan_hash}, anchors: {len(plan_anchors)})")
123
+
124
+
125
+ if __name__ == "__main__":
126
+ from lib.base.hook_utils import run_hook
127
+ run_hook(main, "plan_accepted")
@@ -0,0 +1,81 @@
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")
@@ -29,12 +29,16 @@ import tempfile
29
29
  from pathlib import Path
30
30
  from typing import Any, Dict
31
31
 
32
- # Add lib directory to path for imports
32
+ # Add lib directories to path for imports
33
33
  _hook_dir = Path(__file__).resolve().parent
34
34
  _lib_dir = _hook_dir.parent / "lib"
35
+ _shared_lib = _hook_dir.parent.parent / "_shared" / "lib"
35
36
  sys.path.insert(0, str(_lib_dir))
37
+ sys.path.insert(0, str(_shared_lib))
36
38
 
37
- from utils import eprint, sanitize_filename
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
38
42
 
39
43
 
40
44
  # ---------------------------
@@ -78,7 +82,7 @@ def load_config(project_dir: Path) -> Dict[str, Any]:
78
82
  section = full_config.get("stuckDetection", {})
79
83
  return {**DEFAULT_CONFIG, **section}
80
84
  except Exception as e:
81
- eprint(f"[suggest-fresh-perspective] Failed to load config: {e}")
85
+ log_warn("suggest-fresh-perspective", f"Failed to load config: {e}")
82
86
  return DEFAULT_CONFIG.copy()
83
87
 
84
88
 
@@ -144,7 +148,7 @@ def save_state(session_id: str, state: Dict[str, Any]) -> None:
144
148
  try:
145
149
  state_path.write_text(json.dumps(state), encoding="utf-8")
146
150
  except Exception as e:
147
- eprint(f"[suggest-fresh-perspective] Warning: failed to save state: {e}")
151
+ log_warn("suggest-fresh-perspective", f"Failed to save state: {e}")
148
152
 
149
153
 
150
154
  # ---------------------------
@@ -226,19 +230,15 @@ def should_suggest(state: Dict[str, Any], cooldown: int) -> bool:
226
230
  return state.get("tool_calls_since_suggestion", 0) >= cooldown
227
231
 
228
232
 
229
- def create_suggestion() -> Dict[str, Any]:
230
- """Create the suggestion output."""
231
- return {
232
- "hookSpecificOutput": {
233
- "additionalContext": (
234
- "\n---\n"
235
- "**Stuck?** You've been working on similar issues for a while. "
236
- "Consider running `/fresh-perspective` to get an unbiased view of the problem "
237
- "without code context anchoring your thinking.\n"
238
- "---\n"
239
- )
240
- }
241
- }
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
242
 
243
243
 
244
244
  def main() -> int:
@@ -249,8 +249,8 @@ def main() -> int:
249
249
  except json.JSONDecodeError:
250
250
  return 0 # Fail-safe
251
251
 
252
- # 1. Check hook_type (cheap dict lookup)
253
- if payload.get("hook_type") != "PostToolUse":
252
+ # 1. Check hook_event_name (cheap dict lookup)
253
+ if payload.get("hook_event_name") != "PostToolUse":
254
254
  return 0
255
255
 
256
256
  # 2. Check session_id exists (cheap dict lookup)
@@ -314,11 +314,11 @@ def main() -> int:
314
314
 
315
315
  if is_stuck:
316
316
  if error_detected:
317
- eprint("[suggest-fresh-perspective] Detected repeated error pattern")
317
+ log_info("suggest-fresh-perspective", "Detected repeated error pattern")
318
318
  if file_edit_detected:
319
- eprint("[suggest-fresh-perspective] Detected repeated file edits")
319
+ log_info("suggest-fresh-perspective", "Detected repeated file edits")
320
320
  if test_failure_detected:
321
- eprint("[suggest-fresh-perspective] Detected repeated test failures")
321
+ log_info("suggest-fresh-perspective", "Detected repeated test failures")
322
322
 
323
323
  # Only suggest if stuck AND past cooldown
324
324
  if is_stuck and should_suggest(state, cooldown):
@@ -329,11 +329,12 @@ def main() -> int:
329
329
 
330
330
  # Only suggest up to maxSuggestions times per session
331
331
  if state["suggestion_count"] <= max_suggestions:
332
- eprint(f"[suggest-fresh-perspective] Suggesting fresh perspective (suggestion #{state['suggestion_count']})")
333
- print(json.dumps(create_suggestion(), ensure_ascii=False))
332
+ log_info("suggest-fresh-perspective", f"Suggesting fresh perspective (suggestion #{state['suggestion_count']})")
333
+ create_suggestion()
334
334
 
335
335
  return 0
336
336
 
337
337
 
338
338
  if __name__ == "__main__":
339
- raise SystemExit(main())
339
+ from base.hook_utils import run_hook
340
+ run_hook(main, "suggest_fresh_perspective")
@@ -11,12 +11,21 @@
11
11
  | `utils.py` | Core utilities: eprint, sanitize, JSON parsing, artifact writing |
12
12
  | `state.py` | Plan state file management and iteration tracking |
13
13
  | `orchestrator.py` | Plan complexity analysis and agent selection |
14
- | `reviewers.py` | CLI and agent-based plan review implementations |
15
- | `atomic_write.py` | Atomic file writes for crash safety |
16
- | `constants.py` | Shared constants and configuration |
17
- | `async_archive.py` | Async plan archival operations |
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) |
18
17
  | `__init__.py` | Package exports |
19
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
+
20
29
  ---
21
30
 
22
31
  ## Dependency Graph
@@ -25,8 +34,8 @@
25
34
  Hooks (cc-native-plan-review.py, etc.)
26
35
 
27
36
  ├── lib/utils.py (core utilities)
28
- │ └── lib/atomic_write.py
29
37
  │ └── lib/constants.py
38
+ │ └── _shared/lib/base/atomic_write.py
30
39
 
31
40
  ├── lib/state.py (state management)
32
41
  │ └── lib/utils.py (eprint)
@@ -34,6 +43,14 @@ Hooks (cc-native-plan-review.py, etc.)
34
43
  ├── lib/orchestrator.py (agent selection)
35
44
  │ └── lib/utils.py (ReviewerResult, etc.)
36
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
+
37
54
  └── _shared/lib/ (shared across all methods)
38
55
  ├── lib/base/subprocess_utils.py
39
56
  ├── lib/base/constants.py
@@ -117,7 +134,8 @@ This is a recurring issue. Any path string comparison must handle both separator
117
134
  For critical files (state, reviews), use atomic writes to prevent corruption on crash:
118
135
 
119
136
  ```python
120
- from atomic_write import atomic_write
137
+ # Import from shared lib (canonical location)
138
+ from _shared.lib.base.atomic_write import atomic_write
121
139
 
122
140
  # CORRECT - atomic write
123
141
  success, error = atomic_write(path, content)
@@ -130,14 +148,16 @@ if not success:
130
148
  path.write_text(content, encoding="utf-8")
131
149
  ```
132
150
 
133
- Atomic writes use a temp file + rename pattern. Check `constants.ENABLE_ROBUST_PLAN_WRITES` for the feature flag.
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.
134
152
 
135
153
  ---
136
154
 
137
155
  ## Adding New Reviewers
138
156
 
139
- 1. **Create reviewer function** in `reviewers.py`:
157
+ 1. **Create reviewer file** in `reviewers/` package (e.g., `reviewers/myreviewer.py`):
140
158
  ```python
159
+ from .base import ReviewerResult, REVIEW_SCHEMA
160
+
141
161
  def run_myreviewer_review(
142
162
  plan: str,
143
163
  schema: Dict[str, Any],
@@ -154,7 +174,10 @@ Atomic writes use a temp file + rename pattern. Check `constants.ENABLE_ROBUST_P
154
174
  )
155
175
  ```
156
176
 
157
- 2. **Export in `__init__.py`** (if needed for external use)
177
+ 2. **Export in `reviewers/__init__.py`**:
178
+ ```python
179
+ from .myreviewer import run_myreviewer_review
180
+ ```
158
181
 
159
182
  3. **Add config** in `plan-review.config.json`:
160
183
  ```json
@@ -222,7 +245,7 @@ content = path.read_text() # May use cp1252 on Windows
222
245
  These are reminders based on past issues. Not enforcement rules.
223
246
 
224
247
  - **Don't import from `_cc-native/lib/` in `_shared/lib/`** - wrong direction, creates circular deps
225
- - **Don't use `print()` for debugging** - use `eprint()` to avoid corrupting stdout
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`)
226
249
  - **Don't modify data class fields** without updating all consumers (hooks, formatters, tests)
227
250
  - **Don't hardcode paths** - use `Path(__file__)`, env vars, or config
228
251
  - **Don't forget `encoding="utf-8"`** on file operations - Windows defaults are unsafe
@@ -237,4 +260,6 @@ These are reminders based on past issues. Not enforcement rules.
237
260
 
238
261
  | Date | Change |
239
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` |
240
265
  | 2026-02-03 | Initial creation |
@@ -1,20 +1,35 @@
1
1
  """
2
2
  Permanent debug logging for cc-native hooks.
3
3
 
4
- Logs are written to context folder: _output/contexts/<context-id>/debug/<session-name>.log
4
+ Thin delegation layer over the unified logger (_shared/lib/base/logger.py).
5
+ Logs are written to context folder: _output/contexts/<context-id>/debug/hook-log.jsonl
5
6
  Append-only, cleaned up when context is archived.
6
7
  Can be disabled via CCNATIVE_DEBUG_DISABLE=1 environment variable.
7
8
  """
8
9
 
9
- import json
10
10
  import os
11
- from datetime import datetime
12
11
  from pathlib import Path
13
12
  from typing import Any, Optional
14
13
 
15
14
  # Feature flag - set CCNATIVE_DEBUG_DISABLE=1 to turn off
16
15
  DEBUG_ENABLED = os.environ.get("CCNATIVE_DEBUG_DISABLE", "").lower() not in ("1", "true", "yes")
17
16
 
17
+ # Import unified logger
18
+ try:
19
+ from _shared.lib.base.logger import hook_log
20
+ except ImportError:
21
+ # Fallback: try relative import path used by hooks
22
+ try:
23
+ import sys
24
+ _shared = Path(__file__).parent.parent.parent.parent / "_shared"
25
+ if str(_shared) not in sys.path:
26
+ sys.path.insert(0, str(_shared))
27
+ from lib.base.logger import hook_log
28
+ except ImportError:
29
+ # Last resort: no-op
30
+ def hook_log(*args, **kwargs):
31
+ pass
32
+
18
33
 
19
34
  def get_debug_dir(context_path: Path) -> Path:
20
35
  """Get or create debug directory within context folder.
@@ -51,7 +66,7 @@ def debug_log(
51
66
  message: str,
52
67
  data: Optional[Any] = None
53
68
  ) -> None:
54
- """Write a debug log entry (append-only).
69
+ """Write a debug log entry. Delegates to unified logger.
55
70
 
56
71
  Args:
57
72
  context_path: Path to context folder
@@ -63,22 +78,15 @@ def debug_log(
63
78
  if not DEBUG_ENABLED:
64
79
  return
65
80
 
66
- try:
67
- log_path = get_log_path(context_path, session_name)
68
- timestamp = datetime.now().isoformat()
69
-
70
- entry = f"[{timestamp}] [{component}] {message}"
71
- if data is not None:
72
- try:
73
- data_str = json.dumps(data, indent=2, ensure_ascii=True, default=str)
74
- entry += f"\n{data_str}"
75
- except Exception:
76
- entry += f"\n<data serialization failed: {type(data)}>"
77
-
78
- with open(log_path, "a", encoding="utf-8") as f:
79
- f.write(entry + "\n\n")
80
- except Exception:
81
- pass # Never fail on debug logging
81
+ hook_log(
82
+ "debug",
83
+ session_name,
84
+ message,
85
+ component=component,
86
+ data=data,
87
+ context_path=context_path,
88
+ stderr=False,
89
+ )
82
90
 
83
91
 
84
92
  def debug_raw(
@@ -89,7 +97,7 @@ def debug_raw(
89
97
  raw: str,
90
98
  max_len: int = 10000
91
99
  ) -> None:
92
- """Log raw output (stdout, stderr, etc).
100
+ """Log raw output (stdout, stderr, etc). Delegates to unified logger.
93
101
 
94
102
  Args:
95
103
  context_path: Path to context folder
@@ -104,7 +112,14 @@ def debug_raw(
104
112
 
105
113
  truncated = raw[:max_len] if len(raw) > max_len else raw
106
114
  suffix = f" [TRUNCATED from {len(raw)} chars]" if len(raw) > max_len else ""
107
- debug_log(context_path, session_name, component, f"{label}{suffix}:", truncated)
115
+ hook_log(
116
+ "debug",
117
+ session_name,
118
+ f"{label}{suffix}: {truncated}",
119
+ component=component,
120
+ context_path=context_path,
121
+ stderr=False,
122
+ )
108
123
 
109
124
 
110
125
  def cleanup_debug_folder(context_path: Path) -> None: