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
@@ -45,292 +45,226 @@ Hook JSON excludes system prompt, tools, MCP tokens. We add a baseline
45
45
  to compensate (~22.6k tokens typical). See:
46
46
  https://github.com/anthropics/claude-code/issues/13783
47
47
  """
48
- import json
49
48
  import sys
50
49
  from pathlib import Path
51
- from typing import Optional, Tuple
50
+ from typing import Optional
52
51
 
53
52
  # Add parent directories to path for imports
54
53
  SCRIPT_DIR = Path(__file__).resolve().parent
55
54
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
56
55
  sys.path.insert(0, str(SHARED_LIB.parent))
57
56
 
58
- from lib.base.hook_utils import load_hook_input
59
- from lib.base.utils import eprint, project_dir
60
- from lib.context.context_manager import (
57
+ from lib.base.hook_utils import emit_context, load_hook_input, get_context_percent_remaining, log_debug, log_info, log_warn, log_error, log_diagnostic
58
+ from lib.base.utils import now_iso, project_dir
59
+ from lib.context.context_store import (
61
60
  get_all_contexts,
62
61
  get_context_by_session_id,
63
- update_plan_status,
62
+ maybe_activate,
63
+ save_state,
64
64
  )
65
65
 
66
- # Configuration
67
- LOW_CONTEXT_THRESHOLD = 40 # Warn when below 40% remaining
68
- CRITICAL_CONTEXT_THRESHOLD = 25 # Urgent warning below 25%
69
-
70
- # Context baseline: preloaded tokens not visible to hooks (~22.6k typical)
71
- # This includes system prompt, tools, MCP tokens that aren't in hook data
72
- CONTEXT_BASELINE = 22_600
73
-
74
- # Default context window size (used when not provided in hook input)
75
- DEFAULT_CONTEXT_WINDOW = 200_000
66
+ # Module-level flag: only save auto-state once per process lifetime
67
+ _PROGRESSIVE_SAVE_MARKER = ".progressive-save-done"
76
68
 
77
-
78
- def get_context_tokens_from_hook(hook_input: dict) -> Tuple[Optional[int], Optional[int]]:
79
- """
80
- Extract actual token counts from Claude Code hook input.
81
-
82
- Claude Code provides context_window data with actual token counts:
83
- - cache_read_input_tokens: Tokens read from cache
84
- - input_tokens: New input tokens
85
- - cache_creation_input_tokens: Tokens written to cache
86
- - output_tokens: Model output tokens
87
-
88
- Args:
89
- hook_input: Hook input data from Claude Code
90
-
91
- Returns:
92
- Tuple of (tokens_used, max_tokens) or (None, None) if not available
93
- """
94
- context_window = hook_input.get("context_window")
95
- if not context_window:
96
- return None, None
97
-
98
- current_usage = context_window.get("current_usage")
99
- if not current_usage:
100
- return None, None
101
-
102
- # Sum all token types
103
- cache_read = current_usage.get("cache_read_input_tokens", 0) or 0
104
- input_tokens = current_usage.get("input_tokens", 0) or 0
105
- cache_creation = current_usage.get("cache_creation_input_tokens", 0) or 0
106
- output_tokens = current_usage.get("output_tokens", 0) or 0
107
-
108
- content_tokens = cache_read + input_tokens + cache_creation + output_tokens
109
-
110
- # Add baseline for system prompt, tools, MCP tokens not in hook data
111
- tokens_used = content_tokens + CONTEXT_BASELINE
112
-
113
- # Get max context window from hook input
114
- max_tokens = context_window.get("context_window_size") or DEFAULT_CONTEXT_WINDOW
115
-
116
- return tokens_used, max_tokens
69
+ # Configuration
70
+ SAVE_STATE_THRESHOLD = 60 # Silently save auto-state at 60% remaining
71
+ HANDOFF_SUGGEST_THRESHOLD = 30 # Gentle nudge at 30% remaining (70% used)
72
+ HANDOFF_PREPARE_THRESHOLD = 20 # Stronger warning at 20% remaining (80% used)
73
+ CRITICAL_CONTEXT_THRESHOLD = 10 # Urgent warning at 10% remaining (90% used)
117
74
 
118
75
 
119
76
  def get_current_context_id(project_root: Path = None) -> Optional[str]:
120
- """
121
- Determine the current active context.
122
-
123
- Falls back to most recently active context.
124
-
125
- Returns:
126
- Context ID or None if no active context
127
- """
77
+ """Determine the current active context (most recently active)."""
128
78
  contexts = get_all_contexts(status="active", project_root=project_root)
129
79
  if contexts:
130
- return contexts[0].id # Sorted by last_active desc
80
+ return contexts[0].id
131
81
  return None
132
82
 
133
83
 
134
84
  def get_context_warning(
135
85
  percent_remaining: int,
136
- tokens_used: int,
137
- max_tokens: int,
86
+ tokens_used: Optional[int],
87
+ max_tokens: Optional[int],
138
88
  context_id: Optional[str],
139
89
  tool_name: str
140
90
  ) -> str:
141
- """
142
- Generate appropriate warning based on context level.
143
-
144
- Args:
145
- percent_remaining: Percentage of context remaining
146
- tokens_used: Estimated tokens used
147
- max_tokens: Maximum context window
148
- context_id: Current context ID (if any)
149
- tool_name: Tool that triggered this check
91
+ """Generate appropriate warning based on context level."""
92
+ if tokens_used is not None and max_tokens is not None:
93
+ tokens_used_k = tokens_used // 1000
94
+ max_tokens_k = max_tokens // 1000
95
+ usage_line = f"**Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens"
96
+ else:
97
+ usage_line = f"**Estimated usage**: ~{percent_remaining}% remaining"
150
98
 
151
- Returns:
152
- System reminder markdown
153
- """
154
- # Format token counts
155
- tokens_used_k = tokens_used // 1000
156
- max_tokens_k = max_tokens // 1000
99
+ context_line = f"\nContext ID: `{context_id}`" if context_id else ""
157
100
 
158
101
  if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD:
159
- urgency = "CRITICAL"
160
- instruction = "You MUST wrap up immediately and create a handoff document."
161
- else:
162
- urgency = "LOW"
163
- instruction = "Please wrap up your current task and prepare for handoff."
102
+ return f"""<system-reminder>
103
+ ## CRITICAL CONTEXT WARNING ({percent_remaining}% remaining)
164
104
 
165
- context_info = ""
166
- if context_id:
167
- context_info = f"""
168
- To create a handoff document, use the /handoff command or describe:
169
- - What you were working on
170
- - What's completed
171
- - What still needs to be done
172
- - Any important decisions or context
105
+ {usage_line}
106
+ **Triggered by**: {tool_name} tool completion
107
+
108
+ **CRITICAL: Run `/handoff` now before context is compacted.**
109
+ {context_line}
173
110
 
174
- Context ID: `{context_id}`"""
111
+ You are about to lose context. Stop all other work and run `/handoff` immediately.
112
+ </system-reminder>"""
175
113
 
176
- return f"""<system-reminder>
177
- ## {urgency} CONTEXT WARNING ({percent_remaining}% remaining)
114
+ elif percent_remaining <= HANDOFF_PREPARE_THRESHOLD:
115
+ return f"""<system-reminder>
116
+ ## LOW CONTEXT WARNING ({percent_remaining}% remaining)
178
117
 
179
- **Estimated usage**: ~{tokens_used_k}k / {max_tokens_k}k tokens
118
+ {usage_line}
180
119
  **Triggered by**: {tool_name} tool completion
181
120
 
182
- {instruction}
183
- {context_info}
121
+ **Context is getting low. Please finish your current task and run `/handoff`.**
122
+ {context_line}
184
123
 
185
124
  **Actions:**
186
125
  1. Complete your current atomic task (if 1-2 steps away)
187
126
  2. Do NOT start new multi-step work
188
- 3. Create a handoff document summarizing progress
189
- 4. Ask user: "Context is getting low. I've summarized my progress. Should we continue in a new session?"
127
+ 3. Run `/handoff` to generate a handoff document
190
128
  </system-reminder>"""
191
129
 
130
+ else:
131
+ return f"""<system-reminder>
132
+ ## CONTEXT NOTICE ({percent_remaining}% remaining)
133
+
134
+ {usage_line}
135
+ **Triggered by**: {tool_name} tool completion
192
136
 
193
- def check_and_transition_mode(hook_input: dict) -> None:
194
- """
195
- Check if context needs to transition to implementing mode.
137
+ **Consider preparing a handoff soon. When ready, run `/handoff` to generate a handoff document.**
138
+ {context_line}
196
139
 
197
- This handles two cases:
198
- 1. Plan was approved (pending_implementation) and implementation tools are used
199
- 2. Plan was in planning mode but permission_mode is no longer "plan"
200
- (e.g., after /clear which clears permissions and pastes the plan)
140
+ Continue your current work, but avoid starting large new tasks.
141
+ </system-reminder>"""
201
142
 
202
- If we're seeing tool usage (Edit, Write, Bash) and either:
203
- - Context is in "pending_implementation", OR
204
- - Context is in "planning" and permission_mode is not "plan"
205
- We transition to "implementing".
206
143
 
207
- Args:
208
- hook_input: Hook input data from Claude Code
144
+ def check_and_transition_mode(hook_input: dict) -> None:
209
145
  """
210
- # Only transition on tools that indicate implementation work
211
- implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
212
- tool_name = hook_input.get("tool_name", "")
213
-
214
- if tool_name not in implementation_tools:
215
- return
146
+ Check if context mode needs to transition based on tool usage.
216
147
 
148
+ Handles:
149
+ - has_plan + implementation tool -> active (started implementing)
150
+ - idle + implementation tool -> active
151
+ """
217
152
  project_root = project_dir(hook_input)
218
153
  session_id = hook_input.get("session_id")
219
154
 
220
155
  if not session_id:
221
156
  return
222
157
 
223
- # Get context for this session
224
- context = get_context_by_session_id(session_id, project_root)
225
- if not context:
158
+ state = get_context_by_session_id(session_id, project_root)
159
+ if not state:
226
160
  return
227
161
 
228
- if not context.in_flight:
162
+ # Implementation transitions only trigger on implementation tools
163
+ implementation_tools = {"Edit", "Write", "Bash", "NotebookEdit"}
164
+ tool_name = hook_input.get("tool_name", "")
165
+
166
+ if tool_name not in implementation_tools:
229
167
  return
230
168
 
231
- current_mode = context.in_flight.mode
232
169
  permission_mode = hook_input.get("permission_mode", "default")
170
+ maybe_activate(state.id, permission_mode, project_root=project_root, caller="context_monitor")
233
171
 
234
- # Transition from pending_implementation to implementing
235
- if current_mode == "pending_implementation":
236
- eprint(f"[context_monitor] Transitioning {context.id} from pending_implementation to implementing")
237
- update_plan_status(context.id, "implementing", project_root=project_root)
238
172
 
239
- # Transition from planning to implementing if permission_mode is not "plan"
240
- elif current_mode == "planning" and permission_mode != "plan":
241
- eprint(f"[context_monitor] Transitioning {context.id} from planning to implementing (permission_mode={permission_mode})")
242
- update_plan_status(context.id, "implementing", project_root=project_root)
173
+ def _try_progressive_save(hook_input: dict, percent_remaining: int) -> None:
174
+ """Silently save state at SAVE_STATE_THRESHOLD (60%)."""
175
+ try:
176
+ session_id = hook_input.get("session_id", "")
177
+ if not session_id:
178
+ return
243
179
 
180
+ project_root = project_dir(hook_input)
181
+ state = get_context_by_session_id(session_id, project_root)
182
+ if not state:
183
+ return
244
184
 
245
- def check_context_level(hook_input: dict) -> Optional[str]:
246
- """
247
- Check context level and return warning if low.
185
+ from lib.base.constants import get_context_dir
186
+ marker_path = get_context_dir(state.id, project_root) / _PROGRESSIVE_SAVE_MARKER
187
+ if marker_path.exists():
188
+ try:
189
+ saved_session = marker_path.read_text(encoding="utf-8").strip()
190
+ if saved_session == session_id:
191
+ return
192
+ except OSError:
193
+ pass
248
194
 
249
- Optimized for fail-fast: checks cheap conditions first before any file I/O.
195
+ log_info("context_monitor", f"Progressive save at {percent_remaining}% remaining")
250
196
 
251
- Args:
252
- hook_input: Hook input data from Claude Code
197
+ # Just update last_active and save state
198
+ state.last_active = now_iso()
199
+ save_state(state, project_root)
253
200
 
254
- Returns:
255
- System reminder string if context is low, None otherwise
256
- """
257
- # === FAST PATH: No I/O, just dict lookups and math ===
201
+ try:
202
+ marker_path.write_text(session_id, encoding="utf-8")
203
+ except OSError:
204
+ pass
205
+
206
+ except Exception as e:
207
+ log_warn("context_monitor", f"Progressive save error (non-fatal): {e}")
258
208
 
259
- # 1. Try to get context_window data (fast dict access)
260
- tokens_used, max_tokens = get_context_tokens_from_hook(hook_input)
261
209
 
262
- # 2. If no context_window data, exit immediately - can't monitor accurately
263
- if tokens_used is None or max_tokens is None:
264
- # Only log if we want to debug
265
- # eprint("[context_monitor] context_window data unavailable")
266
- return None
210
+ def check_context_level(hook_input: dict) -> Optional[str]:
211
+ """Check context level and return warning if low."""
212
+ tool_name = hook_input.get("tool_name", "Unknown")
213
+ percent_remaining, tokens_used, max_tokens = get_context_percent_remaining(hook_input)
267
214
 
268
- # 3. Calculate percentage (fast math)
269
- remaining = max_tokens - tokens_used
270
- percent_remaining = max(0, min(100, int((remaining / max_tokens) * 100)))
215
+ log_diagnostic("context_monitor", "receive", f"tool={tool_name}, pct_remaining={percent_remaining}",
216
+ inputs={"tool_name": tool_name, "percent_remaining": percent_remaining,
217
+ "tokens_used": tokens_used, "max_tokens": max_tokens})
218
+
219
+ if percent_remaining is None:
220
+ return None
271
221
 
272
- # 4. Most common case: context is fine, exit early
273
- if percent_remaining > LOW_CONTEXT_THRESHOLD:
222
+ if percent_remaining > SAVE_STATE_THRESHOLD:
274
223
  return None
275
224
 
276
- # === SLOW PATH: Only reached when context is low (rare) ===
225
+ if percent_remaining > HANDOFF_SUGGEST_THRESHOLD:
226
+ _try_progressive_save(hook_input, percent_remaining)
227
+ return None
277
228
 
278
- # Log since we're in warning territory
279
- eprint(f"[context_monitor] Context: {percent_remaining}% remaining "
280
- f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
229
+ if tokens_used is not None and max_tokens is not None:
230
+ log_info("context_monitor", f"Context: {percent_remaining}% remaining "
231
+ f"(~{tokens_used//1000}k/{max_tokens//1000}k tokens)")
232
+ else:
233
+ log_info("context_monitor", f"Context: ~{percent_remaining}% remaining (from context.json)")
281
234
 
282
- # Get current context for handoff info (file I/O)
283
235
  project_root = project_dir(hook_input)
284
236
  context_id = get_current_context_id(project_root)
285
237
 
286
- tool_name = hook_input.get("tool_name", "Unknown")
238
+ threshold = ("critical" if percent_remaining <= CRITICAL_CONTEXT_THRESHOLD
239
+ else "prepare" if percent_remaining <= HANDOFF_PREPARE_THRESHOLD
240
+ else "suggest")
241
+ log_diagnostic("context_monitor", "decide", f"Threshold={threshold} at {percent_remaining}%",
242
+ decision=threshold, reasoning=f"{percent_remaining}% remaining",
243
+ inputs={"context_id": context_id, "percent_remaining": percent_remaining})
287
244
 
288
- return get_context_warning(
289
- percent_remaining,
290
- tokens_used,
291
- max_tokens,
292
- context_id,
293
- tool_name
294
- )
245
+ return get_context_warning(percent_remaining, tokens_used, max_tokens, context_id, tool_name)
295
246
 
296
247
 
297
248
  def main():
298
- """
299
- Main entry point for PostToolUse hook.
300
-
301
- Reads hook input from stdin, checks for mode transitions,
302
- and prints system reminder if context is low.
303
- """
249
+ """Main entry point for PostToolUse hook."""
304
250
  try:
305
- # Read hook input using shared utility
306
251
  hook_input = load_hook_input()
307
-
308
252
  if not hook_input:
309
253
  return
310
254
 
311
- # Always check for mode transitions on implementation tools
312
- # This handles the case where /clear pastes the plan with non-plan permission mode
313
255
  check_and_transition_mode(hook_input)
314
256
 
315
- # Check context level
316
257
  warning = check_context_level(hook_input)
317
-
318
258
  if warning:
319
- # Output JSON with additionalContext so Claude sees the warning
320
- # Plain stdout from PostToolUse only goes to verbose mode, not Claude's context
321
- output = {
322
- "hookSpecificOutput": {
323
- "hookEventName": "PostToolUse",
324
- "additionalContext": warning
325
- }
326
- }
327
- print(json.dumps(output))
259
+ emit_context(warning)
328
260
 
329
261
  except Exception as e:
330
- eprint(f"[context_monitor] ERROR: {e}")
331
262
  import traceback
332
- eprint(traceback.format_exc())
263
+ tb = traceback.format_exc()
264
+ from lib.base.hook_utils import log_hook_error
265
+ log_hook_error("context_monitor", e, "PostToolUse", traceback_str=tb)
333
266
 
334
267
 
335
268
  if __name__ == "__main__":
336
- main()
269
+ from lib.base.hook_utils import run_hook
270
+ run_hook(main, "context_monitor")
@@ -28,17 +28,17 @@ SCRIPT_DIR = Path(__file__).resolve().parent
28
28
  SHARED_LIB = SCRIPT_DIR.parent / "lib"
29
29
  sys.path.insert(0, str(SHARED_LIB.parent))
30
30
 
31
- from lib.base.hook_utils import load_hook_input
32
- from lib.base.utils import eprint, project_dir
31
+ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_error
32
+ from lib.base.utils import project_dir
33
33
  from lib.base.constants import (
34
34
  get_context_plans_dir,
35
35
  get_context_handoffs_dir,
36
36
  get_context_reviews_dir,
37
37
  get_context_file_path,
38
38
  )
39
- from lib.context.context_manager import (
39
+ from lib.context.context_store import (
40
40
  get_context_by_session_id,
41
- get_all_in_flight_contexts,
41
+ get_all_contexts,
42
42
  get_context,
43
43
  )
44
44
 
@@ -66,7 +66,7 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
66
66
  context_file = get_context_file_path(context_id, project_root)
67
67
  if context_file.exists():
68
68
  files.append(str(context_file))
69
- eprint(f"[file-suggestion] Found context file for {context_id}")
69
+ log_debug("file-suggestion", f"Found context file for {context_id}")
70
70
 
71
71
  # Get plans directory
72
72
  plans_dir = get_context_plans_dir(context_id, project_root)
@@ -75,7 +75,7 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
75
75
  # Sort by modification time, most recent first
76
76
  plan_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
77
77
  files.extend([str(p) for p in plan_files])
78
- eprint(f"[file-suggestion] Found {len(plan_files)} plans in {context_id}")
78
+ log_debug("file-suggestion", f"Found {len(plan_files)} plans in {context_id}")
79
79
 
80
80
  # Get handoffs - prefer folder-based (index.md in subdirectories), fall back to legacy
81
81
  handoffs_dir = get_context_handoffs_dir(context_id, project_root)
@@ -92,14 +92,14 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
92
92
  index_file = handoff_folders[0] / "index.md"
93
93
  if index_file.exists():
94
94
  files.append(str(index_file))
95
- eprint(f"[file-suggestion] Found handoff folder: {handoff_folders[0].name}")
95
+ log_debug("file-suggestion", f"Found handoff folder: {handoff_folders[0].name}")
96
96
  else:
97
97
  # Legacy support: flat .md files directly in handoffs/
98
98
  legacy_handoffs = [f for f in handoffs_dir.glob("*.md") if f.is_file()]
99
99
  legacy_handoffs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
100
100
  if legacy_handoffs:
101
101
  files.append(str(legacy_handoffs[0])) # Only most recent legacy
102
- eprint(f"[file-suggestion] Found {len(legacy_handoffs)} legacy handoffs in {context_id}")
102
+ log_debug("file-suggestion", f"Found {len(legacy_handoffs)} legacy handoffs in {context_id}")
103
103
 
104
104
  # Get reviews - prefer folder-based (index.md in subdirectories), fall back to legacy
105
105
  reviews_dir = get_context_reviews_dir(context_id, project_root) / "cc-native"
@@ -116,13 +116,13 @@ def get_context_files(context_id: str, project_root: Path) -> List[str]:
116
116
  index_file = review_folders[0] / "index.md"
117
117
  if index_file.exists():
118
118
  files.append(str(index_file))
119
- eprint(f"[file-suggestion] Found review folder: {review_folders[0].name}")
119
+ log_debug("file-suggestion", f"Found review folder: {review_folders[0].name}")
120
120
  else:
121
121
  # Legacy support: flat review.md directly in cc-native/
122
122
  legacy_review = reviews_dir / "review.md"
123
123
  if legacy_review.exists():
124
124
  files.append(str(legacy_review))
125
- eprint(f"[file-suggestion] Found legacy review.md in {context_id}")
125
+ log_debug("file-suggestion", f"Found legacy review.md in {context_id}")
126
126
 
127
127
  return files
128
128
 
@@ -147,16 +147,17 @@ def get_active_context_id(session_id: str, project_root: Path) -> Optional[str]:
147
147
  if session_id and session_id != "unknown":
148
148
  context = get_context_by_session_id(session_id, project_root)
149
149
  if context:
150
- eprint(f"[file-suggestion] Found context by session: {context.id}")
150
+ log_debug("file-suggestion", f"Found context by session: {context.id}")
151
151
  return context.id
152
152
 
153
- # Fall back to single in-flight context
154
- in_flight = get_all_in_flight_contexts(project_root)
155
- if len(in_flight) == 1:
156
- eprint(f"[file-suggestion] Using single in-flight context: {in_flight[0].id}")
157
- return in_flight[0].id
153
+ # Fall back to single active (non-idle) context
154
+ active = [c for c in get_all_contexts(status="active", project_root=project_root)
155
+ if c.mode != "idle"]
156
+ if len(active) == 1:
157
+ log_debug("file-suggestion", f"Using single active context: {active[0].id}")
158
+ return active[0].id
158
159
 
159
- eprint(f"[file-suggestion] No unique context found (in-flight: {len(in_flight)})")
160
+ log_debug("file-suggestion", f"No unique context found (active: {len(active)})")
160
161
  return None
161
162
 
162
163
 
@@ -179,7 +180,7 @@ def main():
179
180
  project_root = project_dir(hook_input)
180
181
  session_id = hook_input.get("session_id", "unknown")
181
182
 
182
- eprint(f"[file-suggestion] Session: {session_id[:8]}..., Project: {project_root}")
183
+ log_debug("file-suggestion", f"Session: {session_id[:8]}..., Project: {project_root}")
183
184
 
184
185
  # Determine active context
185
186
  context_id = get_active_context_id(session_id, project_root)
@@ -194,19 +195,21 @@ def main():
194
195
  # Limit suggestions to prevent overwhelming the context
195
196
  MAX_SUGGESTIONS = 10
196
197
  if len(suggestions) > MAX_SUGGESTIONS:
197
- eprint(f"[file-suggestion] Limiting suggestions to {MAX_SUGGESTIONS} (was {len(suggestions)})")
198
+ log_debug("file-suggestion", f"Limiting suggestions to {MAX_SUGGESTIONS} (was {len(suggestions)})")
198
199
  suggestions = suggestions[:MAX_SUGGESTIONS]
199
200
 
200
201
  # Output suggestions as JSON array
201
- eprint(f"[file-suggestion] Suggesting {len(suggestions)} files")
202
+ log_info("file-suggestion", f"Suggesting {len(suggestions)} files")
202
203
  print(json.dumps(suggestions))
203
204
 
204
205
  except Exception as e:
205
- eprint(f"[file-suggestion] ERROR: {e}")
206
206
  import traceback
207
- eprint(traceback.format_exc())
207
+ tb = traceback.format_exc()
208
+ from lib.base.hook_utils import log_hook_error
209
+ log_hook_error("file-suggestion", e, "SessionStart", traceback_str=tb)
208
210
  print("[]")
209
211
 
210
212
 
211
213
  if __name__ == "__main__":
212
- main()
214
+ from lib.base.hook_utils import run_hook
215
+ run_hook(main, "file_suggestion")
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """PreCompact hook - saves auto-state before context compaction.
3
+
4
+ Critical: saves state before context compaction destroys token history.
5
+ After compaction, SessionStart fires with source="compact" and the
6
+ restored auto-state provides continuity context.
7
+
8
+ Hook input (from Claude Code):
9
+ {
10
+ "hook_event_name": "PreCompact",
11
+ "session_id": "abc123",
12
+ "transcript_path": "/path/to/transcript.jsonl",
13
+ "cwd": "/path/to/project",
14
+ ...
15
+ }
16
+
17
+ Hook output:
18
+ - Silent (no stdout output needed)
19
+ - Logs to stderr for debugging
20
+ """
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Add parent directories to path for imports
25
+ SCRIPT_DIR = Path(__file__).resolve().parent
26
+ SHARED_LIB = SCRIPT_DIR.parent / "lib"
27
+ sys.path.insert(0, str(SHARED_LIB.parent))
28
+
29
+ from lib.base.hook_utils import load_hook_input, log_debug, log_info, log_error
30
+ from lib.base.utils import project_dir
31
+ from lib.context.context_store import get_context_by_session_id, save_state
32
+
33
+
34
+ def main():
35
+ """Save auto-state before compaction."""
36
+ try:
37
+ hook_input = load_hook_input()
38
+ if not hook_input:
39
+ return
40
+
41
+ session_id = hook_input.get("session_id", "")
42
+ transcript_path = hook_input.get("transcript_path")
43
+ project_root = project_dir(hook_input)
44
+
45
+ if not session_id:
46
+ log_debug("pre_compact", "No session_id, skipping")
47
+ return
48
+
49
+ log_info("pre_compact", f"Saving state before compaction: {session_id[:8]}...")
50
+
51
+ # Find context bound to this session
52
+ context = get_context_by_session_id(session_id, project_root)
53
+ if not context:
54
+ log_debug("pre_compact", "No context bound to this session, skipping")
55
+ return
56
+
57
+ # Save last_session snapshot directly to state.json
58
+ import subprocess
59
+ git_state = {}
60
+ try:
61
+ branch = subprocess.run(
62
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
63
+ capture_output=True, text=True, timeout=5
64
+ )
65
+ git_state["branch"] = branch.stdout.strip() if branch.returncode == 0 else "unknown"
66
+
67
+ status = subprocess.run(
68
+ ["git", "status", "--short"],
69
+ capture_output=True, text=True, timeout=5
70
+ )
71
+ if status.returncode == 0 and status.stdout.strip():
72
+ git_state["uncommitted_files"] = [
73
+ line.split(None, 1)[-1] for line in status.stdout.strip().split("\n")[:10]
74
+ ]
75
+
76
+ log = subprocess.run(
77
+ ["git", "log", "-1", "--format=%h %s"],
78
+ capture_output=True, text=True, timeout=5
79
+ )
80
+ if log.returncode == 0:
81
+ git_state["last_commit_short"] = log.stdout.strip()
82
+ except Exception:
83
+ pass
84
+
85
+ from lib.base.utils import now_iso
86
+ context.last_session = {
87
+ "session_id": session_id,
88
+ "saved_at": now_iso(),
89
+ "save_reason": "pre_compact",
90
+ "git_state": git_state,
91
+ }
92
+ save_state(context, project_root)
93
+ log_info("pre_compact", f"State saved for {context.id}")
94
+
95
+ except Exception as e:
96
+ import traceback
97
+ tb = traceback.format_exc()
98
+ from lib.base.hook_utils import log_hook_error
99
+ log_hook_error("pre_compact", e, "PreCompact", traceback_str=tb)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ from lib.base.hook_utils import run_hook
104
+ run_hook(main, "pre_compact")